Build 452 v0.9.8

**NOTE**: It's recommended to consult changelogs with a Markdown editor/viewer for a clearer structure (and the .MD file in this folder).

---

## Kinky Extended CAS

### Context

- KW did not have a dedicated CAS flow for directly editing the 'Naked' outfit.
- The base game hides or protects various parts of the 'Naked' category in CAS, especially in the Full CAS and appearance panels.
- NRaas Master Controller / Integration offers similar flows, but uses its own pickers and CAS pipeline.

KW Goal:
- Add dedicated menu interactions for editing the naked outfit;
- Make the naked outfit selectable in CAS via a controlled proxy;
- Prevent Tattoo and Body Hair from permanently forcing the underwear preview;
- Maintain the opt-in feature, to reduce risk and conflicts with complex setups.

### Implemented interactions

Added a new CAS submenu under the KW pie menu.

Implemented interaction labels:

```
Edit Nude (Mirror)
Edit Nude (Stylist)
Edit Nude (Dresser)
Edit Nude (CAS)
```

Implementation file:

```
Project/Source Code/Oniki_KinkyMod/Oniki.Interactions/Sim_EditNudeCAS.cs
```

Current behavior:

- interactions target human Sims only;
- pregnancy / visibly pregnant Sims are rejected;
- Grim Reaper service Sims are rejected;
- volatile KW SimData is rejected;
- EP11 bots are accepted only when KW treats them as droids;
- interaction is player-directed only;
- interaction requires `Kinky Extended CAS` to be enabled.

### Feature toggle

Added opt-in setting:

```
KinkyExtendedCAS
Default: false
Menu: Miscellaneous > Kinky Extended CAS
Label (may vary): Kinky Extended CAS
```

Added CAS resource-filter setting:

```
FilterResourcesInCAS
Default: false
Menu: Miscellaneous > Kinky Extended CAS
Label (may vary): Filter Resources in CAS
```

Added world outfit switch setting:

```
GetSimNakedWhileEnteringCAS
Default: false
Menu: Miscellaneous > Kinky Extended CAS
Label (may vary): Get Sim Naked While Entering CAS
```

Added EA-style physical propagation setting:

```
ApplyPhysicalPropertiesToEveryOutfit
Default: true
Menu: Miscellaneous > Kinky Extended CAS
Label (may vary): Apply Physical Properties To Every Outfit
```

Added EA-style body hair propagation setting:

```
ApplyBodyHairToEveryOutfit
Default: true
Menu: Miscellaneous > Kinky Extended CAS
Label (may vary): Apply Body Hair To Every Outfit
```

Added settings container:

```
Menu: Miscellaneous > Kinky Extended CAS
```

Implementation files:

```
Project/Source Code/Oniki_KinkyMod/Oniki/Settings.cs
Project/Source Code/Oniki_KinkyMod/Oniki.UI/OptionSettingMenuMisc.cs
```

Safety contract:

- `OFF`:
  - interaction `Test()` returns false;
  - `Run()` aborts before any CAS work;
  - `PrepareNudeCAS()` aborts before proxy / unlock / hook setup;
  - no Career proxy, no Everyday proxy, no CAS unlock, no event hook.
- `ON`:
  - interactions become available;
  - KW opens a session-scoped Nude CAS flow;
  - KW shows a short non-modal "preparing" notification after the click is accepted;
  - all CAS hooks and unlocks are restored during cleanup.
- `Filter Resources in CAS ON`:
  - KW applies the nude-only CAS part filter.
- `Filter Resources in CAS OFF`:
  - KW falls back to the previous full unlock behavior and exposes all CAS parts through `0xFFFFF`.
- `Get Sim Naked While Entering CAS ON`:
  - KW switches the world Sim to `Naked:0` before CAS and leaves / reapplies naked after cleanup.
- `Get Sim Naked While Entering CAS OFF`:
  - KW still loads CAS on the naked proxy;
  - KW does not force the world Sim naked before CAS;
  - KW restores the Sim's pre-CAS world outfit after cleanup when possible.
- `Apply Physical Properties To Every Outfit ON`:
  - KW propagates EA global physical properties from `Naked:0` to all outfits after the original non-Naked snapshots are restored;
  - propagated components are `BodyShape`, `FacialBlends`, and `SkinTone`;
  - this covers slider/body-shape style appearance edits without letting EA's CAS close rewrite regular outfit clothing.
- `Apply Physical Properties To Every Outfit OFF`:
  - physical changes remain scoped to the edited `Naked:0` outfit.
- `Apply Body Hair To Every Outfit ON`:
  - KW propagates `CASUniformComponents.BodyHair` from `Naked:0` to all outfits after the original non-Naked snapshots are restored;
  - this includes KW pubic hair / stomach body hair where those CAS parts are valid for the outfit.
- `Apply Body Hair To Every Outfit OFF`:
  - body hair changes remain scoped to the edited `Naked:0` outfit.

If the toggle is turned off while a CAS session is already open, the current session should finish and cleanup normally. The toggle controls future entry into the feature.

Cooldown:

- after a Nude CAS session finishes cleanup, KW starts a configurable cooldown;
- current default is `2` Sim minutes;
- Nude CAS interactions are greyed out during cooldown;
- the cooldown uses Sim time, so it does not expire while the game is paused;
- world quit / world load now reset the cooldown, so the greyed-out tooltip does not persist after leaving and re-entering a save;
- a still-active session also blocks new Nude CAS entries as busy;
- a real in-progress preparation window blocks new Nude CAS entries as preparing, before the CAS transition starts;
- logs use `phase=cooldown`, `phase=cooldown_reset`, `phase=prepare_notice`, and `reason=nude_cas_cooldown` / `reason=nude_cas_busy` / `reason=nude_cas_preparing`.

Tested cooldown follow-up:

- an external tester reported that the greyed-out Kinky CAS tooltip could survive quit-to-menu / quit-game and still be active after loading the save again;
- this was not treated as outfit corruption, but as an unwanted persistence of session timing;
- KW now clears the Nude CAS cooldown on both world quit and world load through `Sim_EditNudeCAS.ResetNudeCasCooldown(...)`;
- intended contract: cooldown is session-local protection after CAS cleanup, not a persisted save penalty.

User feedback while CAS is preparing:

- user testing showed that there can be a short delay after clicking a Kinky CAS interaction, before EA's CAS UI appears;
- the delay is expected because KW prepares a safe Nude CAS session first: outfit snapshots, proxy outfit creation, unlock state capture, and diagnostics;
- KW now shows a non-modal notification while this preparation starts, so the player gets immediate feedback that the click was accepted;
- if the player tries to click again during the real preparation window, the interaction is greyed out with a preparing tooltip;
- the preparing state is cleared as soon as `PrepareNudeCAS(...)` finishes, so it is not a long-lived cooldown and does not remain active during the whole CAS session.

### CAS proxy design

KW uses existing TS3 categories instead of creating a new custom CAS category.

Main design:

- prepare / validate `OutfitCategories.Naked`, index `0`;
- clone the naked outfit into temporary proxy slots;
- use a temporary `Career` slot as the clickable visible `Naked` category in CAS;
- relabel the Career CAS button to a localized `Naked` label;
- load CAS on the Career proxy;
- commit the edited proxy back into `OutfitCategories.Naked`, index `0` on save;
- remove all temporary proxy slots during cleanup.

Everyday cleanup hardening:

- repeated save tests showed EA CAS can create extra `Everyday` outfit slots while saving a Nude CAS session;
- the old cleanup removed only `ProxyEverydayIndex`, which could miss the original proxy if EA inserted/replaced an Everyday slot during save;
- KW now removes every `Everyday` slot created beyond the session's `OriginalEverydayCount`, matching the Career proxy cleanup model;
- log counter: `removedTemporaryEverydaySlots`.

Non-Naked outfit restore guard:

- user-provided logs showed EA CAS can replace EA nude parts in regular outfits during CAS close, before KW cleanup starts;
- this happened even when the CAS resource filter was disabled, so the root cause is EA's save validation for non-Naked categories rather than the nude-only grid filter;
- KW now captures cloned snapshots of all pre-existing non-`Naked` outfits before creating proxy slots;
- after committing the edited proxy into `Naked:0` and removing temporary proxy slots, KW restores all non-`Naked` categories from the pre-CAS snapshot;
- intended contract: Kinky Extended CAS persists only the edited `Naked` outfit and does not let EA rewrite `Everyday`, `Formalwear`, `Sleepwear`, `Swimwear`, `Athletic`, `Outerwear`, or other regular categories during close;
- when `Get Sim Naked While Entering CAS` is `OFF`, KW now reapplies the original world outfit with an immediate no-spin switch, so the visible Sim should not remain in EA's temporary/randomized CAS-close outfit while the game is paused;
- log phases: `outfit_restore_snapshot`, `restore_original_outfits`, `after_restore_original_outfits`.

Tested outfit preservation fixes:

- reported symptom: after editing the naked outfit, regular outfits that intentionally used EA nude CAS parts could be rewritten into random EA clothing;
- confirmed reproduction path: NRaas Master Controller / Integration can expose nude CAS parts in regular categories when configured to disable clothing filters / show hidden parts;
- KW's first hardening step preserves CAS cache flags for parts already used by the Sim's existing outfits, so the nude-only resource filter does not hide parts that are already part of the Sim's wardrobe;
- EA CAS can still sanitize regular categories during close, so KW now treats `cas_closed` changes as temporary native damage and restores all non-`Naked` snapshots afterward;
- repeated tests confirmed that `Everyday:0`, `Everyday:1`, and `Swimwear:0` could be altered at `cas_closed`, but returned to their pre-CAS parts by `after_restore_original_outfits` / `after_post_cleanup`;
- `Naked:0` remains the only outfit intentionally updated from the edited proxy.

Temporary outfit slot cleanup:

- EA CAS can create additional temporary `Everyday` slots while saving the Nude CAS proxy;
- KW now removes all `Everyday` slots beyond the original count, not only the originally recorded proxy index;
- the Career proxy uses the same count-based cleanup model and restores the previous `CareerOutfitIndex`;
- validated counters: `removedTemporaryEverydaySlots=2` and `removedTemporaryCareerSlots=1` in the reported tests, returning to `Everyday` count `2` and `Career` count `1`.

Paused-game visual restore:

- reported symptom: with `Get Sim Naked While Entering CAS` set to `OFF`, the Sim could briefly remain visually dressed in EA's temporary/randomized CAS-close outfit while the game was paused;
- root cause: the original world outfit restore was queued with a delayed switch, so the visual update could wait until simulation resumed;
- KW now reapplies the original world outfit immediately during cleanup;
- validation logs show `phase=restore_world_outfit result=true original=Everyday:0`, and in-game tests no longer showed temporary replacement while paused.

Important runtime phases currently logged:

```
create_proxy
create_career_proxy
prepare_notice
unlock_hidden
unlock_parts
load_cas
career_ui_proxy
refresh_career_proxy
appearance_preview_guard
appearance_proxy_dirty
appearance_proxy_debounce
appearance_proxy_sync
cas_closed
commit
remove_proxy
remove_career_proxy
restore_career_index
restore_original_outfits
physical_property_propagation
body_hair_propagation
restore_world_outfit
post_cleanup
restore_unlock
cooldown
cooldown_reset
```

### Unlock behavior

Added session-scoped CAS unlock behavior:

- temporarily clears `CASPart.HiddenInCAS` handling for the target session;
- applies a nude-only CAS part filter instead of exposing every loaded outfit part;
- injects naked outfit parts into CAS grids when required;
- restores backed-up CAS part flags during cleanup.

This unlock is not global by design. It is bound to the Nude CAS session and restored afterward.

Current nude-only filter behavior:

- keeps grid parts already used by the Sim's naked / proxy outfit;
- preserves CAS cache flags for upper/lower/full-body/shoes parts already used by any pre-existing outfit on the Sim, so EA CAS save validation does not treat those outfit parts as missing;
- keeps KW / EA registered naked upper and lower body parts;
- keeps CASP entries marked naked;
- keeps EA-style naked category-flag parts;
- rejects parts that do not match the Sim's age / gender / species through KW's existing `OutfitTools.PartMatches(...)`;
- scrubs normal upper / lower / full-body / shoes parts from the outfit grids during the session.

This is intended to reduce CAS load and avoid human Sims seeing unrelated grid parts such as Plumbot body pieces while editing the naked outfit.

Female CAS filter note:

- outfit-current naked parts are accepted before the generic EA `PartMatches(...)` check;
- generic revealing-only parts are not catalog-whitelisted, because female CC can mark many ordinary clothing pieces as revealing.

Current log counters:

```
filter=nude_only
filter=full_unlock
keptNudeParts
preservedOutfitParts
hiddenGridParts
untouchedParts
injectedNudeParts
```

Additional per-outfit diagnostic logs can appear as:

```
phase=outfit_part
```

These log the actual naked / proxy outfit body parts, their keys, category flags, whether the CAS catalog contains the part, and whether the nude-only filter would expose it.

Additional outfit snapshot diagnostics:

```
phase=outfit_snapshot
phase=outfit_snapshot_item
```

These record outfit counts, outfit keys, and upper/lower/full-body/shoes part keys at key lifecycle checkpoints (`prepare_before_proxy`, `after_load_cas`, `cas_closed`, `after_commit`, `after_remove_everyday_proxy`, `after_remove_career_proxy`, `after_restore_original_outfits`, `after_post_cleanup`). They are intended to diagnose reports where EA nude CAS parts in non-Naked outfits are replaced by random clothing after saving CAS.

### CAS cache timing fix

Patch added after repeated-entry testing showed the second Nude CAS opening could enter CAS while EA `CASLogic.mCasParts` was still empty:

- KW now follows the EA interaction order more closely:
  - prepare proxy / session;
  - clear `HiddenInCAS`;
  - transition into the requested CAS mode;
  - then call `CASLogic.LoadSim(...)`;
  - then apply the Nude CAS part unlock and refresh the clothing grid only after the unlock has succeeded.
- Rationale:
  - EA `LoadSim(...)` only applies the Sim immediately when CAS is already active;
  - loading before transition could leave KW observing an empty CAS part cache during repeated sessions;
  - if the clothing grid initialized from that empty or unpatched cache, the second CAS entry could show EA-filtered lists even with full unlock enabled.
- Expected validation:
  - first and second Nude CAS sessions should both show unlocked naked/custom upper and lower parts;
  - logs should no longer show repeated `phase=unlock_parts result=false reason=cas_parts_empty` before the clothing grid settles;
  - `phase=clothing_grid_refresh` should happen only after a successful `phase=unlock_parts result=true`.

Follow-up after the first timing patch:

- repeated-entry logs showed `unlock_parts result=true` could still be followed by a smaller EA-style visible list (`upperVisible/lowerVisible/shoesVisible` lower than the good session);
- this indicates EA can reload or replace `CASLogic.mCasParts` after KW has already marked the unlock as applied;
- KW now verifies that the current CAS part cache is still in the expected unlocked/filter state on later loop passes;
- if the cache has reset, KW logs `phase=unlock_parts result=false reason=cache_reset_reapply`, recaptures clean backups, reapplies the unlock, and allows the clothing grid refresh to run again.
- Performance follow-up:
  - the cache verification is now limited to the pre-grid-refresh window;
  - after `phase=clothing_grid_refresh result=true`, KW stops scanning all CAS parts every loop pass;
  - rationale: `filter=nude_only` requires expensive per-part checks across the full CAS cache, so repeating that scan after the grid is already corrected can cause severe CAS UI lag.
- Second performance follow-up:
  - with `filter=nude_only`, KW now delays the CAS part unlock until the clothing grid is actually visible;
  - the expensive filter scan no longer runs repeatedly while CAS is loading into non-clothing states;
  - the reset/reapply verification is performed immediately before `clothing_grid_refresh`, where the filtered lists are needed.

Validated result:

- repeated Nude CAS re-entry is stable after cooldown;
- `Filter Resources in CAS` works both `ON` and `OFF`;
- runtime toggle changes in Live Mode are respected by the next Nude CAS entry;
- `filter=nude_only` now remains lightweight during CAS load and UI use;
- validated for both male and female Sims.

### Naked category UI

The visible `Naked` category is currently implemented by proxying the hidden / special flow through `Career`.

Reason:

- EA already has category UI machinery for `Career`;
- a fully custom outfit category button would be more fragile;
- NRaas also exposes related category behavior through existing EA CAS paths.

Current label source:

```
Oniki.KinkyMod.CAS.NudeOutfitCategory.Label
EN: Naked
```

The localization lookup uses the KW key directly and falls back to `Naked` if the key is missing.

### Tattoo and Body Hair handling

EA CAS normally forces an underwear preview in:

- Tattoos
- Body Hair

KW now guards that preview while in Nude CAS:

- detects `CASPhysicalState.Tattoos` and `CASPhysicalState.BodyHair`;
- redirects the model back to the naked Career proxy;
- keeps Tattoo / Body Hair edits visible on the naked body;
- marks the appearance proxy dirty when CAS parts / presets / colors change;
- syncs the proxy after a debounce window.

Tattoo slider drag hardening:

- Body Hair debounce: `250ms`
- Tattoo debounce: `1500ms`
- backoff for unstable current outfit: `750ms`

Rationale:

- Tattoo size / opacity sliders can fire many `preset_applied` events while the mouse is held down.
- Syncing the proxy too aggressively during this burst can produce transient or corrupt native outfit states.
- The longer Tattoo debounce waits for a quieter window before cloning the proxy.

### Physical and Body Hair propagation

EA Full CAS does not treat every Appearance panel item as one single global bucket. The relevant native global propagation path is component-based.

KW now exposes this behavior as two explicit Kinky Extended CAS toggles:

- `ApplyPhysicalPropertiesToEveryOutfit`:
  - default `true`;
  - applies `CASUniformComponents.BodyShape`, `CASUniformComponents.FacialBlends`, and `CASUniformComponents.SkinTone`;
  - updates the SimDescription body shape / skin tone state from the committed `Naked:0` outfit;
  - runs after `restore_original_outfits`, so the non-Naked outfit snapshot restore cannot roll the propagated physical properties back.
- `ApplyBodyHairToEveryOutfit`:
  - default `true`;
  - applies `CASUniformComponents.BodyHair` from the committed `Naked:0` outfit;
  - runs after `restore_original_outfits`, for the same rollback-avoidance reason.

Important boundary:

- physical propagation intentionally does not include makeup, tattoos, hair style, or ordinary clothing;
- body hair propagation is separate so users can disable it without disabling physical slider / skin-tone propagation;
- if either toggle is `OFF`, the corresponding edits remain scoped to `Naked:0`.

Validated behavior:

- height / head-size style slider changes made through Kinky CAS can persist on regular outfits when physical propagation is enabled;
- the initial rollback issue was fixed by moving propagation after KW restores the pre-CAS non-Naked outfit snapshots;
- body hair / pubic hair edits made through the Body Hair panel can now be synchronized across outfits when the new body-hair toggle is enabled.

Related Shave cleanup:

- `Shave` no longer removes only the single pubic hair resource saved from `Naked:0`;
- KW now removes every CAS part whose key or parent key is registered in `FemalePubicHairKeys` or `MalePubicHairKeys` from all outfits visited by the outfit operation;
- the saved naked preset is still kept for regrowth, but cleanup now covers mismatched KW pubic hair resources left on other outfits after Kinky CAS edits.

Source files:

```
Project/Source Code/Oniki_KinkyMod/Oniki.Interactions/Sim_EditNudeCAS.cs
Project/Source Code/Oniki_KinkyMod/Oniki.Gameplay/OutfitManager.cs
Project/Source Code/Oniki_KinkyMod/Oniki.Utilities/OutfitTools.cs
```

### Female Body Hair exposure

The Body Hair button can be shown for female Sims during Nude CAS when KW detects loaded female pubic hair resources.

Implementation constraints:

- uses `CASLogic.mCasParts`;
- checks against KW female pubic hair keys;
- avoids unsafe broad visible-part queries during CAS load;
- remains scoped to Nude CAS.

### Logging and diagnostics

Diagnostics are gated by:

```
Miscellaneous > Logging Features > SimOutfitLogging
EnableGlobalBuffer
```

Main log prefix:

```
[KW-NUDE-CAS]
```

The log writes to:

```
General_Logging_Export_*.xml
```

Crash-safe logging was added because native TS3 access violations do not produce managed ScriptError files.

Current compromise:

- the per-event / periodic crash-safe General log snapshots have been disabled again because Nude CAS no longer appears to crash natively in current tests;
- normal world-quit General log export remains the main diagnostic output;
- General log still dumps early if the buffer grows too large, to avoid unbounded runtime memory growth.

This keeps repeated Nude CAS analysis readable by reducing the number of `General_Logging_Export_*.xml` files produced during one test run.

### NRaas compatibility observations

Validated informally with:

- NRaas Master Controller
- NRaas Master Controller Integration

Observed behavior:

- no obvious conflict in logs;
- KW and NRaas appear to enter CAS through separate interaction / picker flows;
- KW proxy setup and cleanup completed correctly with NRaas installed;
- temporary Everyday / Career proxy counts returned to baseline;
- event hooks and unlocks were restored.

Current policy:

- do not auto-disable Kinky Extended CAS when NRaas is installed;
- keep Kinky Extended CAS as a manual opt-in toggle;
- if a conflict is found later, document the exact NRaas module / flow and gate only the affected behavior.

### Current STBL keys

Settings container:

```
Oniki.KinkyMod.OptionSettings.MenuKinkyExtendedCAS.Label
EN: Kinky Extended CAS
```

Setting:

```
Oniki.KinkyMod.OptionSettings.KinkyExtendedCAS
EN: Kinky Extended CAS
```

CAS resource filter:

```
Oniki.KinkyMod.OptionSettings.FilterResourcesInCAS
hash: not computed (S3SE/S3PE not authorized)
EN: Filter Resources in CAS
```

World outfit switch:

```
Oniki.KinkyMod.OptionSettings.GetSimNakedWhileEnteringCAS
hash: not computed (S3SE/S3PE not authorized)
EN: Get Sim Naked While Entering CAS
```

Physical property propagation:

```
Oniki.KinkyMod.OptionSettings.ApplyPhysicalPropertiesToEveryOutfit
hash: not computed (S3SE/S3PE not authorized)
EN: Apply Physical Properties To Every Outfit
```

Body hair propagation:

```
Oniki.KinkyMod.OptionSettings.ApplyBodyHairToEveryOutfit
hash: not computed (S3SE/S3PE not authorized)
EN: Apply Body Hair To Every Outfit
```

Cooldown setting:

```
Oniki.KinkyMod.OptionSettings.KinkyCasCooldown
hash: not computed (S3SE/S3PE not authorized)
EN: Kinky CAS Cooldown

Oniki.KinkyMod.OptionSettings.KinkyCasCooldown.EditPrompt
hash: not computed (S3SE/S3PE not authorized)
EN: Enter Kinky CAS cooldown in Sim minutes. Use 0 to disable the cooldown.
```

Cooldown tooltips:

```
Oniki.KinkyMod.CAS.NudeOutfitCooldownTooltip
hash: not computed (S3SE/S3PE not authorized)
EN: Kinky Extended CAS is cooling down. Try again in {0} Sim minutes.

Oniki.KinkyMod.CAS.NudeOutfitBusyTooltip
hash: not computed (S3SE/S3PE not authorized)
EN: Kinky Extended CAS is still closing. Try again shortly.

Oniki.KinkyMod.CAS.NudeOutfitPreparingTooltip
hash: not computed (S3SE/S3PE not authorized)
EN: Kinky Extended CAS is preparing. Try again shortly.

Oniki.KinkyMod.CAS.NudeOutfitPreparingNotification
hash: not computed (S3SE/S3PE not authorized)
EN: Preparing Kinky Extended CAS. Please wait...
```

Naked category Label (may vary):

```
Oniki.KinkyMod.CAS.NudeOutfitCategory.Label
EN: Naked
```

Interaction labels currently use raw `UITools.Localize(...)` keys with spaces and parentheses:

```
Oniki.KinkyMod.Edit Nude (Mirror)
EN: Edit Nude (Mirror)

Oniki.KinkyMod.Edit Nude (Stylist)
EN: Edit Nude (Stylist)

Oniki.KinkyMod.Edit Nude (Dresser)
EN: Edit Nude (Dresser)

Oniki.KinkyMod.Edit Nude (CAS)
EN: Edit Nude (CAS)
```

Current path note:

- `CAS...` menu path is currently literal `"CAS" + Localization.Ellipsis`;
- it is not localized through STBL yet;
- if localized, the intended key would be:

```
Oniki.KinkyMod.CAS
EN: CAS
```

Hashes still need to be confirmed through S3SE / target package export before package STBL import.

### Validation so far

Confirmed in tests:

- Full CAS can open on the naked proxy;
- switch from other categories back to `Naked` loads the saved naked outfit, not random generated clothing;
- male and female naked CAS parts appear after unlock fixes;
- Tattoo preview returns to naked after EA underwear preview;
- Body Hair preview returns to naked after EA underwear preview;
- pubic hair / stomach body hair edits can be previewed and saved;
- physical properties from `Naked:0` can be propagated to every outfit when `ApplyPhysicalPropertiesToEveryOutfit` is enabled;
- body hair from `Naked:0` can be propagated to every outfit when `ApplyBodyHairToEveryOutfit` is enabled;
- repeated category switching works;
- repeated Nude CAS entry after cleanup/cooldown works consistently;
- resource filtering `ON` shows the small naked/custom list and remains responsive;
- full unlock `OFF`/`ON` runtime toggling works in Live Mode;
- repeated save/accept no longer leaves extra `Everyday` outfit slots;
- regular outfits that intentionally use EA nude parts are restored after CAS close instead of being permanently replaced by random EA clothing;
- temporary EA changes visible in `cas_closed` logs are gone by `after_restore_original_outfits` and `after_post_cleanup`;
- when `Get Sim Naked While Entering CAS` is `OFF`, exiting CAS while paused no longer leaves the Sim temporarily visible in EA's randomized close outfit;
- propagation now runs after non-Naked outfit restoration, so EA/KW cleanup does not revert the synchronized physical/body-hair changes;
- quit-to-menu / quit-game followed by world load resets the Nude CAS cooldown instead of preserving the greyed-out tooltip;
- clicking a Kinky CAS interaction now gives immediate feedback while the safe pre-CAS preparation runs;
- repeated clicks during that preparation are blocked with a preparing greyed-out tooltip;
- validated on both male and female Sims;
- NRaas Master Controller and Integration did not show obvious runtime conflict;
- no recent ScriptError was produced for successful test runs.

Build status:

- touched C# files were checked for UTF-8 / NUL issues after edits;

### Follow-up

- Decide whether CAS path label should be localized:
  - patch `"CAS" + Localization.Ellipsis` to `UITools.Localize("CAS") + Localization.Ellipsis`;
  - add `Oniki.KinkyMod.CAS`.
- Consider a stricter Tattoo strategy if corruption returns:
  - defer Tattoo proxy sync until leaving the Tattoo state or committing CAS;
  - avoid cloning transient slider-drag frames.
- Add STBL entries for all keys above.
- Keep watching native `xcpt` reports:
  - if crashes recur, compare the last crash-safe `General_Logging_` snapshot against the native crash timestamp.

---

## WooHoo skill journal partner row visibility controls

### Change

- The WooHoo skill journal's **Most Frequent Partner** row can now show the number of counted partner acts for that Sim.
- The existing `PartnerMostFrequent` STBL entry now supports:

```
Most Frequent Partner: {0.SimName} ({1.Number})
```

- Added a new **Global Options -> Journal Settings** container.
- The new container appears immediately after **Pubic Hairs** and before **General Gameplay Settings**.
- Added per-row visibility toggles, all defaulting to `Enabled`, in the same order the partner rows currently appear in the journal:
  - **Enable View - First partner**
  - **Enable View - First virginity partner**
  - **Enable View - Last partner**
  - **Enable View - Most frequent partner**
  - **Enable View - Female partners**
  - **Enable View - Male partners**
  - **Enable View - Futanari partners**
  - **Enable View - Robot partners**
  - **Enable View - Animal partners**
  - **Enable View - Total distinct partners**
- Turning a row `Disabled` hides only that row from the WooHoo skill journal UI.
- If every visible partner row is disabled, or no enabled partner row has data to show, the partner section title is hidden as well.

### Scope

- This is a journal/UI visibility feature only.
- Partner recording, first/last partner tracking, most-frequent partner counting, distinct partner categorization, save data, export/import, and travel merge behavior are unchanged.
- Existing saves receive the new visibility keys through add-if-missing integrity; existing serialized values are preserved.
- Defaults preserve the previous journal display until the player changes the new toggles.

### New STBL keys

- `Oniki.KinkyMod.Skills.WooHoo.Journal.PartnerMostFrequent`
  - `0x45C2E8699B1945DA`
  - EN: `Most Frequent Partner: {0.SimName} ({1.Number})`

- `Oniki.KinkyMod.OptionSettings.MenuJournalSettings.Label`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Journal Settings`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerFirstAny`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - First partner`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerFirstVirginity`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - First virginity partner`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerLast`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Last partner`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerMostFrequent`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Most frequent partner`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerCountFemale`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Female partners`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerCountMale`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Male partners`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerCountFuta`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Futanari partners`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerCountRobot`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Robot partners`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerCountAnimal`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Animal partners`

- `Oniki.KinkyMod.OptionSettings.JournalViewPartnerCountTotal`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Enable View - Total distinct partners`

### Source files

- `Oniki.Gameplay.Skills/WooHooSkill.cs`
- `Oniki/Settings.cs`
- `Oniki.UI/OptionSettingMenuGlobal.cs`

---

## Advanced settings menu while KW is disabled

### UI change

- When Kinky World is disabled, the **Advanced** settings menu no longer exposes **Import Settings** or **Export Settings**.
- The disabled-state Advanced menu now keeps only the disabled-safe **Enhanced Basic Autonomy** control, plus **Uninstall** when the save has previously enabled KW.
- **Import Settings**, **Export Settings**, and **Reset All Settings** remain available when KW is enabled.
- When **Import Settings** returns a failure code and shows **Failed to import settings!**, KW now writes a diagnostic MiniScriptError with the returned error code, KW enabled state, and runtime buffer snapshot.

### Reason

- Import/export are persistence operations for the full KW settings object and are clearer/safest when KW is active.
- This avoids confusing disabled-state workflows where import can still trigger settings application logic after loading values.
- The diagnostic MiniScriptError gives testers a useful file to provide even when the imported settings have disabled the normal runtime buffer.

### Source file

- `Oniki.UI/OptionSettingMenuAdvanced.cs`
- `Oniki.UI/ImportSettings.cs`

---

## Relationship Panel Kinky Traits / ReFiner compatibility

### Fix

- Exposed a public `Oniki.UI.Hud.TryAddKinkyTraitsRelationshipPanelInteraction(...)` helper so external relationship-panel menu rebuilders can add KW's **Kinky Traits** entry without duplicating KW internals.
- KW's own relationship-panel pie menu now uses the same helper before showing the menu, keeping the existing Kinky Traits visibility checks centralized in one place.
- This allows ReFiner's custom **Ask to be invited over** relationship-panel hook to coexist with KW's **Kinky Traits** entry instead of replacing the click flow that KW extends.

### Scope

- No new setting, tuning value, migration, package resource, or STBL entry.
- The helper only adds the relationship-panel Kinky Traits interaction when the active Sim and target both pass the existing `SimTools.CanUseKinkyTraits(...)` checks.

### Source file

- `Oniki.UI/Hud.cs`

---

## Kinky trait social discovery cleanup

### Change

- `Casual Talk`, `Kinky Talk`, and `Share Kinky Secret` now reveal one unknown visible Kinky trait whenever possible.
- Added **Kinky Trait Discovery Chance** under `Miscellaneous > Social Interactions Settings`; default is `100%`, preserving the new guaranteed behavior unless the player lowers it.
- Removed the old social discovery cooldown and relationship-weighted probability roll from the centralized Kinky trait reveal path.
- `Share Kinky Secret` now remains available when the only remaining shareable information is an unknown Kinky trait.
- `Share Kinky Secret` tries the centralized Kinky trait reveal before falling back to its older contextual secret payloads.
- Removed Kinky trait discovery from `Seduce` and `Tease`, keeping discovery concentrated on conversational/social-secret interactions.

### Safety

- Social discovery candidates are restricted to actual KW Kinky traits from the unified Kinky trait state, including active KW reward-like traits such as `IncestReward`.
- Hidden KW traits, unmet-pack traits, EA lifetime rewards, and University/social-group traits are not candidates.
- Incest variants are covered through the existing incest definitions/action paths for the same interactions.

### New STBL keys

- `Oniki.KinkyMod.OptionSettings.KinkyTraitDiscoveryChance`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Kinky Trait Discovery Chance`
- `Oniki.KinkyMod.OptionSettings.KinkyTraitDiscoveryChance.EditPrompt`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: `Set the chance that a completed eligible social interaction discovers an unknown Kinky trait.`

### Source files

- `Oniki.Utilities/SimTools.cs`
- `Oniki.Gameplay/SocialCallbacks.cs`
- `Oniki.UI/OptionSettingKinkyTraitDiscoveryChance.cs`
- `Oniki.UI/OptionSettingMenuMisc.cs`
- `Oniki/Settings.cs`
- `Oniki.Interactions/SocializeProxy.cs`
- `Oniki.Interactions/ShareKinkySecret.cs`
- `Oniki.Interactions/Seduce.cs`
- `Oniki.Interactions/Tease.cs`

---

## WooHoo animation package loader hardening and Brisbane POV known package

### Community report

- A player reported that fresh saves could throw a MiniScriptError during KW startup before the Kinky World menu became available.
- The stack pointed to `WooHooStage.LoadLegacy(...)` through EA's `XmlDbData.XmlDbRowFast.Parse(...)`:

```
System.Xml.XmlException: 'Element' is an invalid node type.
```

- After that first startup failure, KW could remain partially initialized: the KW menu was missing, Edit Town behavior could become unstable, and world quit could produce a secondary `Main.Stop()` null-reference error.

### Investigation notes

- Fresh-save startup loads the known animation package list, including `Brisbane Australia`.
- The Brisbane chair pack's `OKW_Brisbane Australia.package` contains a modern `_XML` resource with the correct `Brisbane Australia` instance:

```
0x3621F18E441CCEB3
```

- The XML itself is malformed: it closes the last `WooHooStage` entry but is missing the final `</WooHooStages>` closing tag.
- In local package inspection this malformed file was a plausible candidate for a startup-loader failure because the legacy fallback expects a legacy table/`Position` XML format, not modern `WooHooStages`.
- Follow-up in-game testing showed that the installed Brisbane chair pack can still be loaded and registered without throwing through this path, so Brisbane should be treated as a hardening test case / suspicious malformed third-party XML rather than a confirmed root cause for the original user report.
- The underlying code risk remains: any modern parse failure followed by a legacy fallback failure could abort `Main.Start()` and leave KW loaded only halfway.

### Fix

- `WooHooStage.Load(string)` now wraps the legacy fallback parse in its own `try/catch`.
- If modern or legacy parsing fails, KW logs the package name, exception type, and message, records the load audit / early-load trace, writes the failure to the exception log channel, treats that package as loaded `0`, and continues startup.
- Parser failures are written to the exception log channel as well as the runtime buffer so they remain visible even when the normal startup buffer is cleared because `LogKWStartup` is disabled.
- This does not repair malformed third-party XML, but it prevents a bad animation package from preventing KW itself from loading.
- Added `BAPOV` to `sKnownWooHooPackageNames`.
  - `BAPOV` is the documented key for Brisbane Australia's POV animation pack.
  - The inspected POV package XML is valid and uses `_XML` instance `0xB07A5EBE48DCB4BD`.
  - This makes the POV pack eligible for fresh-save known-package startup and the existing silent `CheckKnownPackages` pass.
- Added `WooHooStage.Load("BAPOV")` to the fresh-save hardcoded startup list so brand-new saves try the POV pack during the initial animation-package pass, not only through `Check known packages`.

### Scope

- No package resources were modified.
- `Brisbane Australia` remains in the known package list for compatibility, but malformed copies of that third-party chair XML will be skipped instead of breaking startup.
- `BAPOV` is an additional known package name only; no new setting, migration, tuning value, or STBL entry.

### Source file

- `Oniki.Gameplay/WooHooStage.cs`
- `Oniki/Main.cs`

---

## WooHoo animation Packages menu: remove missing registered package records

### Change

- Added a new **Remove missing packages** action to the WooHoo animation **Packages** settings menu, alongside the existing **Add package**, **Check known packages**, and **Scan for packages** actions.
- The action asks for confirmation before changing the saved `WooHooStagePackages` list.
- On confirmation, KW checks each registered package record and removes entries whose runtime resource can no longer be resolved.
- The result popup reports:
  - `{0.Number}` = removed package records;
  - `{1.Number}` = registered package records checked;
  - a code-appended line list of removed package names, matching the existing appended-list style used by `Check known packages` and `Scan for packages`.

Example result body:

```
{0.Number} packages have been removed from a total of {1.Number} registered packages:
```

The popup then appends removed names on following lines, for example:

```
masteranimations
kw_clydie_animations
```

### Runtime safety

- The purge is conservative: a record is kept if the package is already loaded in the current session, if a modern `WooHooStages` `_XML` resource still exists for its registered name/resource ID, or if the old legacy `Position` XML format is still readable.
- Registered `0x...` entries are treated as direct XML instance IDs instead of being re-hashed as text.
- Normal named entries use the inverse of manual Add Package: build the `_XML` `ResourceKey` from `ResourceUtils.HashString64(registeredName)` and verify that the runtime can still resolve a valid WooHoo animation XML.
- The action only edits the saved registration dictionary. It does not delete package files from disk and does not unload already-loaded stage data from the active session.

### Scope

- No package resources were modified.
- No settings migration, tuning value, or gameplay behavior outside the WooHoo animation package settings UI.
- New code fallbacks provide English strings until package STBL entries are added.

### New STBL keys

- `Oniki.KinkyMod.UI.OptionSetting.PurgeMissingPackages`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: Remove missing packages
- `Oniki.KinkyMod.UI.OptionSetting.PurgeMissingPackages.Confirm`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: This will remove registered WooHoo animation packages that are no longer available at runtime.
- `Oniki.KinkyMod.UI.OptionSetting.PurgeMissingPackages.Title`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: Remove Missing Packages
- `Oniki.KinkyMod.UI.OptionSetting.PurgeMissingPackages.Complete`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: {0.Number} packages have been removed from a total of {1.Number} registered packages:
- `Oniki.KinkyMod.UI.OptionSetting.PurgeMissingPackages.None`
  - hash: not computed (S3SE/S3PE not authorized)
  - EN: No missing package records were found. Registered packages checked: {0.Number}.

### Source files

- `Oniki.Gameplay/WooHooStage.cs`
- `Oniki.UI/OptionSettingMenuPackages.cs`
- `Oniki.UI/OptionSettingPurgeMissingPackages.cs`
- `Oniki_KinkyMod.csproj`

---

## World runtime profiling now exports to a dedicated World_Weight log

### Request

- `WorldRuntimeProfiling` diagnostics for world-load/runtime weight, including `perform`, `OutfitManager`, `OutfitTools`, broadcasters, autonomy-cycle and heartbeat samples, were mixed into `General_Logging_`.

### Change

- KW now stores `WorldRuntimeProfiler` output in a dedicated runtime buffer.
- On world quit, that buffer is exported with the `World_Weight_` prefix instead of `General_Logging_`.
- The exported XML type is `WorldWeightLog`.
- The world runtime session summary is emitted into the same dedicated file.

### Scope

- Only `[KW-WORLD-PROFILE]` lines are moved.
- `General_Logging_` remains the destination for miscellaneous diagnostics such as `[KW-BOOK-READ]`, use-toilet traces, ask-invite traces, pie-menu diagnostics and school-situation profiling.
- `KW_LoadingTimeDebug_` remains unchanged for `LogKWSaveLoadProfile`.

### Source files

- `Oniki/Log.cs`
- `Oniki/KinkyMod.cs`
- `Oniki.Utilities/WorldRuntimeProfiler.cs`

### Temporary diagnostics

- Added `[KW-BOOK-READ]` records to the existing `General_Logging_` export, gated by:
  - `Miscellaneous -> Logging Features -> Miscellaneous Logging`
  - `Enable Global Buffer`
- The trace records:
  - preload singleton replacement for `ReadBook`, `ReadBookChooser`, and `GetBook`;
  - preload/post-load repair counts for already-instantiated books and inventory interaction lists;
  - `CreateInstance()` type for chooser/get/read definitions;
  - `ReadBookChooser.Run()` branch selection, including `ShouldStayInPosture(...)`;
  - continuation push results and queue snapshots before/after;
  - entry/failure/success for `Book_GetBook`, `Book_ReadBook.Run()`, and `Book_ReadBook.RunFromInventory()`.
- This diagnostic is intended to identify why a beach-towel/inventory read command can briefly queue the KW chooser and then be replaced by a vanilla `Sims3.Gameplay.Objects.ReadBook` interaction.
- Log analysis confirmed the replacement cause: `Book_ReadBook.Definition` was private, so external callsites in `Book_ReadBookChooser` and `Book_GetBook` resolved `new Book_ReadBook.Definition(...)` to the inherited EA `ReadBook.Definition` instead of the KW nested definition. The KW definition is now `internal`, so continuation creation produces `Oniki.Interactions.Book_ReadBook`.

---

## Brothel free-time whore loop and OutfitManager busy warnings

### Community report

- On saves with an active brothel, players could frequently see the red warning:

```
OutfitManager is busy.
```

- Affected WooHoo sequences could then start with Sims still dressed because the outfit preparation step was skipped while the outfit manager was already busy.
- Separately, after a brothel employee whore finished a shift but remained on the brothel lot, KW could repeatedly queue `Start Whoring`.
- The queued interaction disappeared almost immediately, then returned again, producing rapid outfit changes every few seconds.

### Root cause

- Brothel whore employees received a strong autonomous `StartWhoring` score simply because they had the brothel whore job.
- That score did not fully respect the current shift state / free-time state.
- `StartWhoring.Run()` then detected the Sim was already a brothel whore and did not start a new street-whore situation, but it still ran the outfit change path.
- At shift end, `StopWorking()` cleared the generic `Work` motive but could leave the active brothel work motive behind, including the custom whore motive.
- Together, those states could create a loop where autonomy kept trying to "start whoring" outside working time, while repeated uniform swaps kept `OutfitManager` busy.

### Fix

- Autonomous `StartWhoring` is now blocked for brothel whore employees when:
  - the assigned brothel is not open; or
  - the employee's schedule says they should not currently be at work.
- Brothel whore employees only receive the special `StartWhoring` autonomous score during valid working time.
- `StartWhoring.Run()` also rechecks the same guard before changing clothes, preventing a race if the shift state changes between scoring and execution.
- `BrothelManager.StopWorking()` now removes the actual current work motive as well as the generic `Work` motive before setting `WorkMotive = None`.

### Player impact

- Whore employees should stop auto-spamming `Start Whoring` during free time while still standing on the brothel lot.
- Shift-end outfit flicker / repeated uniform changes should be gone.
- The related `OutfitManager is busy` warnings during WooHoo setup should be greatly reduced or eliminated in brothel saves, allowing Sims to enter WooHoo outfit preparation normally.

### Scope

- No change to brothel hiring, assignment, schedules, tariffs, customer selection, professional WooHoo scoring, or manual whoring commands.
- No new setting, migration, tuning value, package resource, or STBL entry.

### Source files

- `Oniki.Interactions/StartWhoring.cs`
- `Oniki.Gameplay/BrothelManager.cs`

---

## Premade KW NPC social resets: repaired stale celebrity-manager ownership

### Problem

- ErrorTrap could reset premade Kinky World NPCs after autonomous social interactions completed.
- The issue was first reported while NPCs used `Give Flowers` with ReLover `Full Romance Interactions = FULL`, but the same exception also occurred after unrelated friendly and funny socials such as:
  - `Ask About Day`;
  - `Share Interests`;
  - `Gossip`;
  - `Ask About Partner`;
  - `Talk About Sim In Room`;
  - `Express Need for Exercise`.
- All captured failures ended in the same EA celebrity-system stack:

```
MiniSimDescription.FindMiniRelationship(null)
CelebrityManager.HasBeenImpressedBy(...)
CelebrityManager.CanSocialize(...)
SocialInteractionA.UpdateConversationAfterSocialAnimationFinished(...)
```

### Root cause

- The affected Sims were KW premade NPCs imported from SIME resources and assigned to the NPC household.
- SIME import can retain an existing `CelebrityManager` whose internal owner ID/cache still refers to the identity stored in the imported resource.
- KW then assigns its canonical premade-NPC `SimDescriptionId` directly.
- The `SimDescriptionId` property setter does not propagate that change into attached managers.
- EA `SimDescription.Fixup()` only repairs a celebrity manager when its current owner resolves as null. A stale owner that temporarily resolves can therefore survive fixup and fail later.
- During post-social conversation feedback, `CelebrityManager.HasBeenImpressedBy()` could consequently receive a null owner and throw.
- `Give Flowers` was a visible trigger rather than the defective interaction. ReLover FULL increased how often the affected romantic social could run, but it did not corrupt the NPC identity.

### Fix

- Added centralized `RepairCelebrityManagerOwner(SimDescription)` handling for KW NPC lifecycle paths.
- The helper calls EA's native:

```
CelebrityManager.ResetOwnerSimDescription(current SimDescriptionId)
```

- Applied after:
  - generic SIME import;
  - premade KW NPC creation and fixup;
  - recovery/unpacking of an existing premade NPC;
  - world-load reconciliation of premade NPCs already present in the save.
- World-load reconciliation makes the fix self-healing for existing saves; players do not need to regenerate the NPCs or start a new game.

### Scope

- No changes to `Give Flowers`, EA social execution, romantic buckets, ReLover, social autonomy, celebrity levels, fame, relationships, or NPC selection.
- NPCs without a `CelebrityManager` remain unchanged.
- No new setting, migration, tuning value, package resource, or STBL entry.

### Source file

- `Oniki.Roles/NPC.cs`

---

## Standing toilet exit freeze: corrected `Kinky_toilet` JAZZ transition

### Problem

- Male Sims using a toilet while standing could finish satisfying Bladder but remain frozen inside `Oniki.UseToilet`.
- The interaction stalled while requesting state-machine state `Exit`; depending on SACS timing it could:
  - remain visually frozen for a prolonged period;
  - eventually recover after one or more Sim hours;
  - throw `SacsErrorException`;
  - or throw `SacsTimeoutException: Request timed out. (actor = 'x', state = 'Exit')`.
- Because the state-machine request blocked before the remaining interaction code ran, affected Sims did not reach outfit restoration or `StandardExit()`.
- The failure was probabilistic and could affect different Sims or saves. Sitting toilet use was not affected.

### Root cause

- Runtime diagnostics identified SACS state `0xD063BFB0` as `putDown`, the state used to lower the toilet seat after standing urination.
- `UseToilet` can randomly lower the raised seat and then request `Exit`.
- When no animated flush follows, the custom `Kinky_toilet` graph had no declared `putDown -> Exit` transition.
- Its original `putDown` outbound states were limited to:
  - `clean` (`0x688FD0AC`);
  - `flush` (`0x449C20A1`);
  - `repair` (`0x7D4F4D72`).
- SACS could therefore become idle in `putDown` while the C# interaction waited for `Exit`.
- The `Kinky_toilet` resources extracted from Builds 450, 451 and 452 were byte-identical, confirming that this was a latent graph defect rather than corruption introduced by Build 452 or the KW Services migration.

### Fix

- Updated the `Kinky_toilet` JAZZ resource inside `full_install/ONIKI_Anims.package`.
- Added the missing direct outbound transition:

```
putDown (0xD063BFB0) -> Exit (0x01994745)
```

- Final `putDown` outbound states are:
  - `Exit` (`0x01994745`);
  - `clean` (`0x688FD0AC`);
  - `flush` (`0x449C20A1`);
  - `repair` (`0x7D4F4D72`).
- Resource identity remains:
  - Type: `0x02D5DF13`;
  - Group: `0x00000000`;
  - Instance: `0x370A90F42CA4113B`.

### Binary preservation

- The final resource was patched directly from the original Build 452 binary JAZZ rather than rebuilt through a full SmoothJazz text round-trip.
- Exactly one chunk changed: `JazzState` chunk 71 (`putDown`), increasing from 40 to 44 bytes for the additional transition reference.
- Resource size changed from `28672` to `28676` bytes.
- All 257 chunks remain present.
- All 80 animation nodes remain byte-identical; animation clips, speeds, blend values, flags and timing priorities are unchanged.
- Final patched JAZZ SHA-256:
  - `3CAE84617E63E0E27C94A499091E14846EE9B3F8F811A24D95B488C290E3C564`

### Validation

- Repeated standing toilet use completed without freeze, SACS error, timeout or ScriptError.
- General Logging confirmed progression through:
  - `use_loop.end`;
  - `dirty_inc.after`;
  - `sm_exit.before`;
  - `sm_exit.after result=success`;
  - `redress.swap.after result=True`;
  - `standard_exit.after`;
  - `cleanup.end`.
- The toilet-seat lowering animation retains its original normal speed.
- No change was made to toilet autonomy selection, seat-down probability, flush probability, sitting use, masturbation eligibility or outfit policy.

### Package resource

- `full_install/ONIKI_Anims.package`
- State machine: `Kinky_toilet`

---

## Toilet undressing: footwear now follows `UndressShoes`

### Problem

- The `UseToiletNaked` flow rebuilds a temporary lower-nude outfit before deciding whether the Sim will use the toilet sitting or standing.
- Consequently, both `peeSitting` and `peeStanding` used the same footwear handling.
- With `UndressShoes = ON`, `OutfitManager` intentionally converted `LowerNaked` into `FeetNaked`.
- With `UndressShoes = OFF`, ordinary shoes generally remained equipped, but a `BodyTypes.Shoes` CAS part carrying the `Naked` flag could be detected as already-bare feet.
- That inferred `FeetNaked` state was merged into the temporary toilet outfit and could replace a visible custom shoe with the resolved nude-feet resource.
- This was the same CAS-classification leak fixed for WooHoo preparation, but the toilet flow did not use the WooHoo-only policy.

### Fix

- Added a transient `ToiletFootwearPolicy` marker for temporary outfits created by `UseToilet`.
- Added a separate transient `PreserveFootwearPolicy` marker for the return outfit.
- Both markers are consumed during outfit construction and are not retained in naked-state flags, settings, or save data.

#### Temporary toilet outfit

- `UndressShoes = OFF`:
  - removes `FeetNaked` from the requested toilet-undress flags;
  - removes automatically inferred `FeetNaked` from the source outfit scan;
  - preserves the exact existing `BodyTypes.Shoes` part, whether it is a normal shoe, unusual CC shoe, EA bare feet, or custom bare-foot part.
- `UndressShoes = ON`:
  - explicitly requests `FeetNaked`;
  - preserves an existing `BodyTypes.Shoes` part already marked with the CAS `Naked` flag;
  - otherwise replaces footwear with the valid nude-feet resource for the Sim;
  - continues to support EA nude feet, default replacements using the EA ResourceKey, and custom naked-foot parts.

#### Return outfit

- Toilet redressing always uses `PreserveFootwearPolicy`, independently of the current `UndressShoes` value.
- The original feet/shoes part is restored without being reclassified or normalized from its CAS `Naked` metadata.

### Covered toilet paths

- Standard toilet use with `UseToiletNaked = Always`.
- Standard toilet use with `UseToiletNaked = ArousedOnly` when the arousal/masturbation eligibility condition activates undressing.
- Both sitting and standing toilet animations.
- Lower-body undressing added when toilet masturbation begins after the standard use loop.
- Male Glory Hole presenter lower-body preparation.
- Outfit restoration after ordinary toilet use, toilet masturbation, and presenter preparation.

### Scope

- `UseToiletNaked = Never` remains unchanged unless a later toilet-specific masturbation or Glory Hole path explicitly requires lower-body undressing.
- `PeeOutside` is not included in this pass.
- Explicit manual foot actions such as `AskToShowFeet`, `UndressShoesManual`, `ShowFeet`, and `WooHooRemoveShoes` remain unchanged.
- Kinky Dance and Dance Pole remain unchanged.
- No new player-facing setting, migration, tuning value, or STBL entry was added.

### Cache safety

- Toilet-policy and footwear-preservation builds bypass the ordinary naked-flags-only cached-outfit lookup.
- This prevents outfits with the same naked flags but different shoes/custom feet from reusing an incompatible cached result.

### Validation

- In-game validation matrix:
  - `UseToiletNaked = Always`, sitting, `UndressShoes` OFF and ON;
  - `UseToiletNaked = Always`, standing, `UndressShoes` OFF and ON;
  - `UseToiletNaked = ArousedOnly`, qualifying aroused Sim, sitting and standing;
  - ordinary shoes, visible CC shoes marked `Naked`, EA bare feet, and custom bare-foot parts;
  - toilet masturbation transition and final redress;
  - Glory Hole presenter preparation and final redress.

### Source files

- `Oniki.Utilities/NakedFlags.cs`
- `Oniki.Utilities/OutfitTools.cs`
- `Oniki.Gameplay/OutfitManager.cs`
- `Oniki.Interactions/UseToilet.cs`

---

## WooHoo footwear policy: `UndressShoes` now preserves or removes footwear consistently

### Community report

- With `UndressShoes` / Barefoot disabled, some specific shoes could still disappear when a Sim was prepared for a WooHoo sequence.
- Ordinary shoes generally remained equipped, while affected custom-content shoes disappeared only after KW rebuilt the Sim's partial-nude WooHoo outfit.
- The inconsistent result depended on the CAS metadata of the part occupying the `BodyTypes.Shoes` slot.

### Root cause

- The Sims 3 represents both visible shoes and bare feet through CAS parts in the `BodyTypes.Shoes` slot.
- EA bare feet use the CAS `Naked` category flag. Custom bare-foot meshes and default replacements can use the same classification.
- KW's outfit scan begins with `FeetNaked` assumed and clears it only after finding a `BodyTypes.Shoes` part that is not marked `Naked`.
- A visible shoe incorrectly or unusually marked `Naked` was therefore indistinguishable from a valid bare-foot CAS part.
- During WooHoo outfit reconstruction, the detected `FeetNaked` state could be merged back into the requested flags even when `UndressShoes` was disabled.
- KW then removed the existing `Shoes` part and inserted its resolved nude-feet resource, causing the affected visible shoe to disappear.
- Build 431 forcibly cleared `FeetNaked` when `UndressShoes` was disabled. Later builds relaxed that global rule so explicit foot-related interactions could still work, but the WooHoo preparation paths no longer guaranteed that the existing feet slot remained untouched.

### New WooHoo-only behavior

- Added an internal transient `WooHooFootwearPolicy` marker to outfit requests created specifically while preparing Sims for WooHoo.
- The marker is consumed by `OutfitManager` during outfit construction and is not retained as part of the resulting naked-state flags or saved settings.
- No player-facing setting, migration, tuning value, or STBL entry was added.

#### `UndressShoes = OFF`

- WooHoo preparation removes `FeetNaked` from both:
  - the naked flags requested by the current WooHoo stage;
  - the naked flags inferred automatically from the source outfit's CAS metadata.
- The existing `BodyTypes.Shoes` part is therefore copied into the reconstructed WooHoo outfit without being removed or normalized.
- This preserves exactly what the Sim was already wearing on the feet:
  - normal EA shoes;
  - custom-content shoes;
  - visible shoes carrying unusual or incorrect `Naked` metadata;
  - EA bare feet;
  - custom bare-foot CAS parts.

#### `UndressShoes = ON`

- WooHoo preparation explicitly requests `FeetNaked`, independently of whether the animation stage also removes the lower body.
- If the current `BodyTypes.Shoes` part is already marked with the CAS `Naked` flag, KW treats it as an already-valid bare-foot part and preserves it.
- This allows custom bare-foot parts with their own ResourceKey to survive WooHoo outfit reconstruction.
- If the current part is not marked `Naked`, KW treats it as footwear and replaces it with the valid nude-feet resource resolved for the Sim's age and gender.
- EA nude-feet ResourceKeys remain the fallback.
- A default-replacement foot package is supported automatically because The Sims 3 resolves the replacement content through the same EA ResourceKey.

### Covered WooHoo preparation paths

- Initial participant outfit setup in `WooHooInstance.SimEntry.SetupOutfit`.
- Main participant preparation through `WooHooInstance.MakeSimReady`.
- Added participant preparation through `WooHooInstance.MakeJoinSimReady`.
- Outfit updates during WooHoo stage changes and actor-position swaps, which reuse `SimEntry.SetupOutfit`.
- Generic join preparation in `WooHooJoinGeneric` for both the joining actor and the existing target.
- Shared direct preparation through `WooHooTools.MakeSimReady`.
- Book-based join/start outfit preparation.
- Couch/Sleep-on-Couch WooHoo requester preparation.
- Both synchronous `OutfitManager.ChangeOutfit` calls and asynchronous `SwitchOutfitHelper` requests consume the same WooHoo-only policy.

### Explicitly excluded interactions

- The policy does not change global `OutfitManager` footwear behavior.
- The following explicit/manual foot actions do not receive the `WooHooFootwearPolicy` marker and retain their existing behavior:
  - `AskToShowFeet`;
  - `RequestUndressShoes`;
  - `UndressShoesManual`;
  - `ShowFeet`;
  - `WooHooRemoveShoes`;
  - Kinky Dance shoe-removal events;
  - Kinky Dance on Counter/Table shoe-removal events;
  - Dance Pole shoe-removal events.
- Other non-WooHoo calls that explicitly request `NakedFlags.FeetNaked` are also unchanged.

### Cache safety

- WooHoo footwear-policy outfit construction bypasses the ordinary naked-flags-only outfit-cache lookup.
- This prevents two outfits with identical naked flags but different custom feet/shoes parts from incorrectly sharing a previously cached result.
- The finished outfit may still be cached normally for later manager cleanup, but policy-sensitive construction always starts from the current source outfit.

### Validation

- Audited all `ApplyWooHooFootwearPolicy`, `WooHooFootwearChangeRequired`, and `WooHooFootwearPolicy` callsites.
- Confirmed no policy callsite exists in the explicit/manual foot interactions listed above.
- Source encoding validation passed.
- Experimental Build 452 compilation passed.
- In-game validation matrix:
  - OFF + ordinary shoes: shoes remain;
  - OFF + visible CC shoes marked `Naked`: CC shoes remain;
  - OFF + EA or custom bare feet: existing feet remain;
  - ON + ordinary shoes: shoes are replaced by resolved nude feet;
  - ON + custom feet marked `Naked`: custom feet remain;
  - ON + EA nude-feet default replacement: replacement mesh is resolved through the EA ResourceKey;
  - repeat the same checks for initial WooHoo, generic join, additional participant join, and stage change.

### Source files

- `Oniki.Utilities/NakedFlags.cs`
- `Oniki.Utilities/OutfitTools.cs`
- `Oniki.Gameplay/OutfitManager.cs`
- `Oniki.Gameplay/WooHooInstance.cs`
- `Oniki.Utilities/WooHooTools.cs`
- `Oniki.Interactions/WooHooJoinGeneric.cs`
- `Oniki.Interactions/Book_ReadBook.cs`
- `Oniki.Interactions/Couch_SleepOnCouch.cs`

---

## Performance: reduced background outfit/layer rebuild spikes

### Problem

- After re-enabling EA autonomy ownership through the new Build 452 Miscellaneous toggle, some saves showed much larger runtime spikes than expected.
- The issue was most visible in heavy worlds such as Isla Paradiso, where EA autonomy can keep more Sims active around town than Classic KW autonomy.
- General Logging with world-load profiling showed repeated spikes under:
  - `simdata.perform`;
  - `simdata.perform.outfit`;
  - `outfitmanager.update`;
  - `outfittools.update_layers_and_erection`;
  - `outfittools.update_layers.set_outfit`.
- Many expensive passes were no-op outfit rebuilds:
  - `cached=false`;
  - `key_changed=False`;
  - `changed=False`;
  - `morph_changed=False`.
- In practice, the game could spend hundreds of milliseconds, and sometimes multiple seconds, rebuilding CAS/outfit state for Sims whose visual result did not actually change.

### Root cause

- `OutfitManager.NeedsLayerAndErectionPass()` treated any active KW visual layer buff as a reason to run a full layer/erection pass.
- Long-lived visual buffs such as creampie, grool, belly/body/hands/penis/face/breast/anal layers, and beaten layers could therefore keep requesting maintenance even when the stack/state had not changed.
- A second hidden trigger forced the same expensive path whenever:

```
abs(last applied erection - current SimData erection) > 0.01
```

- That 0.01 threshold acted like a permanent micro-realtime erection monitor, even when `OutfitManagerRealtime` was disabled.
- With EA autonomy ownership enabled, more Sims can remain active in the simulated world, making this background maintenance cost much easier to notice.

### Fix

- Added a lightweight visual-layer signature for the KW layers that actually affect outfit rebuild output.
- The outfit/layer maintenance pass now runs when the visual-layer signature changes, rather than merely because a visual-layer buff is present.
- Removed the unconditional 0.01 erection delta trigger from `NeedsLayerAndErectionPass()`.
- `OutfitManagerRealtime` still keeps its explicit realtime erection/morph path when enabled, using the existing larger threshold:

```
abs(manager erection - SimData erection) > 0.15
```

- With `OutfitManagerRealtime` disabled, KW no longer performs background CAS rebuilds only because arousal/erection drifted by a tiny amount.

### Preserved behavior

- Explicit outfit and WooHoo paths remain unchanged.
- WooHoo preparation can still request naked/partial-naked outfits through `ChangeOutfit`, `SwitchOutfitHelper`, and the normal WooHoo setup paths.
- Explicit erection changes through KW gameplay paths such as `SetErection` / `StartErection` remain available.
- Dirty outfit state, outfit ResourceKey changes, sync requests, cooldown expiry, and real visual-layer stack changes can still force maintenance.
- `OutfitManagerRealtime = ON` still allows live visual erection/morph updates for players who want that behavior.

### Diagnostics

- Expanded world-load profiling made the expensive path visible through `General_Logging_Export_*` when the KW world-load logger is enabled.
- Useful profiler phases include:
  - `simdata.perform.outfit`;
  - `outfitmanager.update`;
  - `outfitmanager.update_layers_and_erection`;
  - `outfittools.update_layers_and_erection`;
  - `outfittools.update_layers.set_outfit`;
  - `outfittools.update_layers.layers`;
  - `outfittools.update_layers.erection`;
  - `outfittools.update_layers.cache_outfit`.
- The log now makes it easier to distinguish real visual changes from expensive no-op rebuilds.
- `outfitmanager.update_layers_and_erection` now includes a `reason=` field for the exact trigger:
  - `dirty`;
  - `current_outfit_key_changed`;
  - `omrt_erection_delta`;
  - `visual_signature_changed`;
  - `last_applied_outfit_key_changed`.
- Outfit layer pass and skip records also include `omrt_enabled=True/False`.
- Toggling `OutfitManagerRealtime` during the same play session emits a profiler marker:

```
scope=settings.outfit_manager_realtime value=True/False source=option_toggle
```

- Skip records include stable/cooldown reasons, so future logs can separate genuine rebuild causes from avoided maintenance.
- Added diagnostic-only fast-skip candidate fields to outfit layer pass and skip records:
  - `fast_skip_candidate=True/False`;
  - `fast_skip_blocked_by=...`;
  - `visual_signature` / `last_visual_signature`;
  - `last_key_invalid`;
  - `resource_valid`;
  - `resource_equals_current`;
  - `dirty`;
  - `sync`;
  - `pending_blends`;
  - `helper_task_active`;
  - `external_switch_helper`;
  - `own_switch_helper`;
  - `pending_outfit`;
  - `waiting`;
  - `lock_active`;
  - `lock_current_task`;
  - `erection_delta`.
- This does not skip `SetOutfit` yet. It only reports whether a future conservative first-touch fast path would have been eligible.
- Follow-up refinement split the old broad `swap_active` diagnostic into explicit helper, pending-outfit, waiting, and lock fields. The manager's own current lock is reported but no longer treated as a fast-skip blocker by the diagnostic candidate test.

### Scope

- No new player-facing setting, migration, tuning value, package resource, or STBL entry.
- No change to EA autonomy ownership logic itself.
- No change to ReFiner / ReHelper Isla Paradiso routing maintenance.
- No change to WooHoo eligibility, arousal thresholds, buffs, CAS resources, or animation flow.

### Validation

- Retest in Isla Paradiso reported a clear reduction in large freeze spikes after the first visual-layer signature pass.
- Follow-up logs confirmed that the remaining large spikes were dominated by no-op layer/erection rebuilds, leading to removal of the 0.01 erection delta trigger.

### Source files

- `Oniki.Gameplay/OutfitManager.cs`
- `Oniki.Utilities/OutfitTools.cs`
- `Oniki.Utilities/WorldRuntimeProfiler.cs`
- `Oniki.Gameplay/SimData.cs`

---

## Old-save compatibility: deferred Service migration

### Problem

- After the non-destructive EA/KW Service conversion work, some existing saves could stall near the beginning of the world loading bar.
- The initial implementation converted and destroyed persisted Service handlers synchronously inside `Main.Start()`.
- Older saves can deserialize EA and KW handlers together with existing requests, created Sims, assigned lots, and active `ServiceSituation` references.
- Mutating `Services.sAllServices` while processing those persisted handlers could therefore perform unsafe conversion work before world loading had fully completed.

### Fix

- `KWServices.Start()` now only installs the global KW manager and captures a stable snapshot of `Services.sAllServices`.
- Before the snapshot, `KWServices.Start()` performs immediate structural sanitation only:
  - removes null slots from `Services.sAllServices`;
  - creates an empty pool when an otherwise valid Service has `mPool == null`;
  - does not replace, merge, convert, or destroy any non-null Service.
- This preserves the safe part of the legacy startup cleanup. It prevents EA `FakeMetaAutonomy.ShouldSimBeRemovedFromService()` from dereferencing a null entry in `Services.AllServices` before deferred migration runs.
- `FakeMetaAutonomy` is now included in the same deferred, non-destructive migration design:
  - its conversion was removed from synchronous `Main.Start()`;
  - the existing pool is sanitized before the first Service simulation without replacing its handler;
  - conversion to `KWFakeMetaAutonomy` runs through the deferred reconciliation pass;
  - pool and Service state transfer through `KWServices.ReplaceService`;
  - the old handler is emptied before `Service.Destroy()` is called;
  - null or invalid `SimDescription` entries are removed before post-load use and before every simulation pass;
  - valid pool members have their `Service` and `CreatedByService` references rebound to the retained handler.
- Follow-up anti-recursion fix:
  - EA `FakeMetaAutonomy.UpdateCreatedSim()` delegates to `SimDescription.CreatedByService.UpdateCreatedSim(sim)`;
  - assigning `CreatedByService` back to the same EA FakeMeta handler caused infinite self-recursion and `StackOverflowException`;
  - while the EA handler is retained, sanitation now recovers the original creating Service by locating the Sim in another Service pool;
  - if no valid original Service can be reconstructed, the Sim is removed from the FakeMeta pool instead of creating a recursive ownership link;
  - after conversion to `KWFakeMetaAutonomy`, self-ownership is safe because the KW override no longer delegates and only applies `CanBeFired = false`.
- `FakeMetaAutonomy.mCreatedSims` and `FakeMetaAutonomy.mSimsAssignedToLots` are treated as transferable retained state rather than proof of incompatible active work.
  - In observed saves, FakeMeta could report `requests=0`, `situations=0`, and non-zero assigned lots, leaving the deferred migration permanently pending until the retry limit.
  - For FakeMeta only, lot assignments are now transferred through `KWServices.ReplaceService` together with the pool and other Service state.
  - Actual pending requests and Service Situations still defer conversion.
- Mail Carrier, Pizza Delivery, Repairman, and Maid reconciliation no longer runs synchronously inside `Main.Start()`.
- The first reconciliation pass is scheduled after the main world-load handler has completed its normal startup work.
- Every pass works from a new immutable snapshot rather than enumerating the live list while handlers are destroyed or added.
- EA-to-KW and KW-to-EA replacement is refused while the source handler has:
  - outstanding lot requests;
  - created service Sims;
  - Sims assigned to lots;
  - active Service Situations.
- Busy handlers remain intact and are retried every five Sim minutes, up to twelve attempts.
- Existing legacy KW handlers matching the enabled toggle are reused and rebound instead of being recreated.
- Exact-class duplicate handlers are merged only when the duplicate being removed is idle.
- Service reconciliation now filters by KW-managed concrete service families, not by `ServiceType` alone:
  - `PizzaDelivery` reconciliation only considers EA `PizzaDelivery` and `KWPizzaDelivery`;
  - third-party services that reuse `ServiceType.PizzaDelivery` for their own delivery systems are ignored and preserved;
  - `AddSafe()` uses the same managed-family filter, so KW service registration cannot merge or replace unrelated mod services that share the same enum value.
- Runtime service toggle changes now request the same deferred reconciliation path instead of converting immediately inside the settings callback.
- Pending migration alarms are removed during `KWServices.Shutdown()`.

### Services logging

- Added a dedicated `KWServicesLog_` export channel for service migration diagnostics.
- The service migration trace no longer writes to `KWErrorLog_`.
- The log is controlled by:
  - `Miscellaneous -> Logging Features -> Services logging`;
  - `Enable Global Buffer`;
  - XML tunable default `Main.kDefaultServicesLogging`.
- Default is `false`, so normal gameplay does not buffer service snapshots.
- When enabled, `[KW-SERVICE-MIGRATION]` records snapshots, reconciliation attempts, duplicate merges, handler transfers, pending retries, and give-up preservation.
- New setting:
  - Full key: `Oniki.KinkyMod.OptionSettings.ServicesLogging`
  - Hash: pending manual S3SE verification
  - EN: `Services logging`
  - Setting dictionary key: `ServicesLogging`
  - Default: `Main.kDefaultServicesLogging` (`false` unless changed by XML tuning)
  - Migration/integrity: add-if-missing; existing values are preserved.
- Tuning XML updated:
  - `Updated Kinky World/ONIKI_KinkyModTuning/S3_0333406C_00000000_06A65D4C444AFBF4_Main%%+_XML.xml`
  - `<kDefaultServicesLogging value="False">`

### Validation

- In-game validation is required with:
  - a save created before the Service preservation patch;
  - a save containing an enabled KW service;
  - toggling ON and OFF while a service is idle;
  - toggling while a Service Situation is active and confirming deferred retry.

---

## LoversLab settings: dedicated KW Services submenu

- Added **KW Services** as the first entry in the LoversLab settings submenu.
- Moved the service controls out of the main LoversLab list and grouped them as:
  - KW Mail Carrier
  - Mail Carrier Gender
  - KW Pizza Delivery
  - Pizza Delivery Gender
  - KW Maid
  - Maid Gender
  - KW Repairman
  - Repairman Gender
- This is a settings UI reorganization only; service defaults, saved values, runtime behavior, and migrations are unchanged.
- New STBL label to add manually:
  - Full key: `Oniki.KinkyMod.OptionSettings.MenuKWServices.Label`
  - Hash: pending manual S3SE verification
  - EN: `KW Services`
- No package or STBL resource was modified.

### Source files

- `Oniki.UI/OptionSettingMenuLoversLab.cs`
- `Oniki.UI/OptionSettingMenuKWServices.cs`
- `Oniki_KinkyMod.csproj`

---

## Zoo Lover progression: DogBreeder natural unlock restored

### Bug

- Female Sims could not naturally unlock the `DogBreeder` reward trait.
- After a max-level Zoo Lover completed vaginal WooHoo with a dog, the reward branch checked for `Horse` a second time instead of checking for `Dog`.
- This was a legacy logic error already present in Build 442; the unified Kinky Traits migration preserved the broken species condition rather than causing it.

### Fix

- `ZooLoverSkill.ReportAction()` now awards:
  - `HorseBreeder` after qualifying vaginal WooHoo with a horse;
  - `DogBreeder` after qualifying vaginal WooHoo with a dog.
- Trait detection and assignment continue to use the unified helpers (`HasAnyTraitUnified` / `AddTraitUnified`).

### Requirements

- The Sim must be female.
- Zoo Lover gameplay must be enabled.
- The Zoo Lover skill must have reached its maximum level.
- The qualifying action must be vaginal WooHoo with a dog.
- Zoo pregnancy type/chance settings are not required to unlock `DogBreeder`; they only control whether and what kind of pregnancy can start afterward.

### Source file

- `Oniki.Gameplay.Skills/ZooLoverSkill.cs`

---

## Interactions Tuning: localized names and clearer table layout

### Problem

- The **Interactions Tuning** picker displayed only the internal tuning identifier and target object.
- Technical identifiers such as `use_pc_masturbate`, `Computer_BrowseWeb`, or generated `TalkAbout...` names could be difficult to associate with the interaction shown to players in the pie menu.
- Interaction labels do not follow one universal STBL pattern:
  - standard KW interactions may use either `:InteractionName` or `.InteractionName`;
  - some definitions use a differently named STBL key;
  - `TalkAbout...` interactions compose their label from a localized topic;
  - some cloned or dynamic interactions reuse an EA label or select between multiple labels at runtime.

### UI change

- The picker now uses three columns:

| Column | Content |
|--------|---------|
| **Resolved name** | Best available localized player-facing interaction name |
| **Technical name** | Original internal tuning/definition identifier |
| **Object** | Object type receiving the tuning, such as `Sim`, `Computer`, or `Phone` |

- The technical identifier remains visible even when a localized name is found, preserving exact diagnostic and tuning identification.

### Name resolver

- Added safe STBL lookup without creating or running interaction instances.
- Resolution now checks:
  - `Oniki.KinkyMod.<ShortName>:InteractionName`;
  - `Oniki.KinkyMod.<ShortName>.InteractionName`;
  - direct `Oniki.KinkyMod.<ShortName>` entries;
  - known aliases where the tuning identifier and interaction STBL key differ;
  - dynamically composed `TalkAbout...` labels;
  - selected EA localization keys for cloned vanilla interactions.
- Known alias coverage includes computer browsing/work, photographs, callgirl enrollment/filtering, underwear, period protection, clothing removal, voyeur interactions, pills, and WooHoo routing.
- Dynamic labels use a representative localized variant where the exact text depends on the actor, target, object subtype, or current state.
- Unresolved entries fall back to a human-readable form of the technical identifier instead of displaying raw underscore/CamelCase text.
- Runtime STBL parameters such as `{0.String}` are shown as `...` in this diagnostic list.

### Scope

- This is a presentation and diagnostics improvement only.
- Existing interaction tuning persistence, autonomous enable/disable behavior, commodity changes, and interaction tests are unchanged.
- Setting **Autonomous** to OFF still persists `InteractionTuning.FlagField.DisallowAutonomous` through the existing `SettingTuning` system.

### STBL

- `Oniki.KinkyMod.Settings.Header.ColumnLabelResolvedName`
  - `0x1F79A5CDEE821284`
  - EN: `Resolved name`
- `Oniki.KinkyMod.Settings.Header.ColumnLabelTechnicalName`
  - `0xBA758F4D5BCDD263`
  - EN: `Technical name`
- The existing object-column key remains:
  - `Oniki.KinkyMod.Settings.Header.ColumnLabelObject`

### Source files

- `Oniki.UI/OptionSettingInteraction.cs`
- `Oniki.UI/OptionSettingMenuInteractions.cs`

---

## EA autonomy ownership: Disable KW Autonomy Manager (Light-style Layer A in Classic)

**Scope:** Integrate the **Kinky World Light (446)** autonomy kill-switch philosophy into **Classic KW 451+** as a **player-facing, save-persisted toggle** with **next-world-load apply** (no hot mid-session manager swap). Includes forensic logging, load-time per-Sim integrity fix, and world-quit manager hygiene.

**Design reference:** `Kinky World Light/CHANGELOG-KWL-0.1`; prior failed experiment `Project/Source Code (edited 444)` (runtime restore Ã¢â‚¬â€ rejected pattern).

### Background (player-visible goal)

- Classic KW always reinjects **`Oniki.Gameplay.AutonomyManager`** and **`KWAutonomy.Convert()`** on every world load and Sim instantiation Ã¢â‚¬â€ full KW autonomy engine.
- **KW Light** instead never starts the KW global manager and never converts per-Sim autonomy Ã¢â‚¬â€ EA keeps **ownership** of who decides (visit lot, meta-autonomy, need routing at engine level).
- Community / author request: bring Light-style **EA ownership** into Classic **without** losing 451 features, and **without** risky mid-session manager surgery (historical idle / starvation regressions when toggling autonomy at runtime).
- Expected player benefit with toggle **ON**: lighter CPU (no KW autonomy simulate loop), fewer Ã¢â‚¬Å“everyone stays on the active lotÃ¢â‚¬Â patterns, more EA-like town circulation Ã¢â‚¬â€ while KW content (interactions, traits, WooHoo, brothel, etc.) remains available via other KW systems.

### Architecture Ã¢â‚¬â€ two layers (explicit product scope)

```
Layer A Ã¢â‚¬â€ WHO DECIDES (ownership)     Ã¢â€ Â this build
  Toggle ON  Ã¢â€ â€™ EA AutonomyManager + Sims3.Gameplay.Autonomy.Autonomy per Sim
  Toggle OFF Ã¢â€ â€™ Oniki.Gameplay.AutonomyManager + KWAutonomy (Classic)

Layer B Ã¢â‚¬â€ WHAT CAN BE SCORED (plumbing)
  KW PreLoad singleton swaps (Eat, Sleep, TV, etc.) Ã¢â‚¬â€ UNCHANGED by this toggle
  Brothel / service / situation autonomy Ã¢â‚¬â€ still KW where not guarded
  Kinky interactions still injectable; EA engine may still pick KW defs when scored
```

**Product decision:** Layer A only for Build 452. **No** revert of KW interaction singletons / full vanilla plumbing pack. Layer B revert remains a separate future decision if ever needed.

### Player workflow

| Step | Behavior |
|------|----------|
| Toggle in **Miscellaneous** | Saves bool immediately; **does not** swap manager or Convert live Sims |
| Confirm dialog | Explains effect on **next world load** (reload save, travel transition, return from main menu) |
| Next world load | Bootstrap reads saved toggle and chooses KW vs EA injection path |
| Toggle OFF again + reload | Full Classic reinjection restored (`Start()` + `Enqueue...` + `Convert`) |

**Reversible:** OFF Ã¢â€ â€™ reload Ã¢â€ â€™ KW classic; ON Ã¢â€ â€™ save Ã¢â€ â€™ reload Ã¢â€ â€™ EA ownership.

---

## New setting: `DisableKWAutonomyManager`

| Field | Value |
|-------|--------|
| Persistable key | `DisableKWAutonomyManager` |
| Default (code + fresh save) | `false` (Classic KW autonomy Ã¢â‚¬â€ unchanged legacy behavior) |
| Tunable XML override (fresh save only) | `Main.kDefaultDisableKWAutonomyManager` in `ONIKI_KinkySettings` `_XML` |
| Helper | `Main.IsKWAutonomyManagerDisabled()` |
| UI | Miscellaneous Ã¢â€ â€™ **Disable KW Autonomy Manager (EA ownership)** |
| Apply timing | **Next world load only** (by design Ã¢â‚¬â€ not a runtime hot swap) |

### XML tunable (modpack default)

| Resource | `Oniki.Main` `_XML` in `ONIKI_KinkySettings.package` |
|----------|--------------------------------------------------------|
| Repo mirror | `Updated Kinky World/ONIKI_KinkyModTuning XMLs/S3_0333406C_00000000_06A65D4C444AFBF4_Main%%+_XML.xml` |
| Field | `<kDefaultDisableKWAutonomyManager value="False">` |
| Priority | Save key **wins** once present; XML applies only when key missing (`ResetSettings` / `EnsureDisableKWAutonomyManagerSettingPresent`) |

Modpack authors can ship `True` for Light-like installs; existing saves keep their serialized value.

---

## Layer A implementation (guarded injection)

When `DisableKWAutonomyManager = true` **after reload**:

| Area | File | Guarded behavior |
|------|------|------------------|
| KW manager start | `Oniki.Gameplay/AutonomyManager.cs` | `Start()` returns immediately Ã¢â‚¬â€ no replacement of `AutonomyManager.sInstance` |
| Active household enqueue | same | `EnqueueActiveHouseholdSelectableAtWorldLoad()` not called from `Main.Start()` |
| Main startup | `Oniki/Main.cs` | Skips `AutonomyManager.Start()` + enqueue block |
| Sim instantiated listener | `Oniki/Main.cs` | Skips `KWAutonomy.Convert(sim.Autonomy)` |
| Sim instantiate path | `Oniki.Utilities/SimTools.cs` | Skips Convert on guarded instantiate |
| Brothel worker autonomy | `Oniki.Gameplay/BrothelManager.cs` | Skips `new KWAutonomy(...)` when disabled |
| Brothel customer setup | `Oniki.Situations/BrothelSituation.cs` | Skips Convert |
| KW manager loop convert | `Oniki.Gameplay/AutonomyManager.cs` | `Start()` guard prevents loop ownership |

**Uninstall path:** `Main.Uninstall()` always calls `AutonomyManager.Uninstall()` (removed erroneous guard that skipped uninstall when toggle ON).

---

## Forensic logger: EA Autonomy Ownership (`KWEAAutonomyLog_*`)

Dedicated buffer channel for validating Layer A after reload Ã¢â‚¬â€ **separate** from `KWAutonomyDebug_*` and `KWFlowTrace_*`.

### Activation gates

| Requirement | Setting |
|-------------|---------|
| Logger ON | `EaAutonomyOwnershipLogging` (Logging Features) |
| Buffer writes | `EnableGlobalBuffer` ON |
| Gameplay toggle | `DisableKWAutonomyManager` should be ON for meaningful audit (logger works regardless, but RED_FLAG semantics assume EA ownership intent) |

**Explicit split:** `AutonomyDebugLogging` controls legacy `KWAutonomyDebug_*` only. It **does not** gate the EA ownership logger.

### Export

- File: `KWEAAutonomyLog_Export_<guid>.xml` under The Sims 3 user folder
- Dump: world quit when buffer non-empty (even if logger toggled OFF before quit Ã¢â‚¬â€ buffer preserved)
- Mid-session toggle ON: arms capture immediately (`runtime_armed`) Ã¢â‚¬â€ not world-load-only

### Log content

| Phase | Purpose |
|-------|---------|
| `load_census` / `load_census_end` | Per-Sim `managerType` + `autonomyType` at `PostLoadFixUp`, `Main.Start` milestones; reports `kwAutonomyCount` |
| `[CONVERT] kw_autonomy_mutation` | Forensic trace on every **executed** Convert / `new KWAutonomy` / `revert_to_ea` with callsite, toggle state, before/after types |
| `world_load_snapshot` (+1s / +30s) | All living world Sims Ã¢â‚¬â€ manager + autonomy type + `inQueue` |
| `autonomous_commit` / `autonomous_enqueue` | Observed EA autonomous activity (probe + `SimTools` queue hooks) |
| `[RED_FLAG]` | Ownership violation: KW manager instance and/or `KWAutonomy` on Sim while EA ownership expected |

**Traced Convert callsites:** `listener.sim_instantiated`, `instantiate.sim`, `fixsim.sim`, `brothel.start_working`, `brothel.setup_customer`, `manager_loop`, `revert_to_ea` (integrity).

### Runtime sync fixes (452)

- `EaAutonomyOwnershipLog.SyncRuntimeWithSettings()` Ã¢â‚¬â€ toggle ON mid-session starts snapshots/probes without requiring reload
- `Settings.SetValue` hook for `EaAutonomyOwnershipLogging` (menu uses `SetValue`, not property setter)
- Quit dump uses `Log.HasEaAutonomyOwnershipBuffer()` Ã¢â‚¬â€ exports accumulated buffer even if toggle flipped off before exit

---

## Bug: residual per-Sim `KWAutonomy` with toggle ON (Layer B integrity)

### Background (player-visible / forensic symptom)

- With `DisableKWAutonomyManager=ON`, forensic export showed **global manager correct** (`Sims3.Gameplay.Autonomy.AutonomyManager`) but **7 Sims** (later **8** in validation save) still had `sim.Autonomy` type **`Oniki.Gameplay.KWAutonomy`**.
- ~246 `RED_FLAG` lines Ã¢â‚¬â€ all attributable to those Sims, not a general toggle failure.
- Same Sim IDs every cold load (e.g. Tanner Whitley selectable, Lola Belle, Harry Marks, Bianca Rubble, Katrina Pala, Richie Striker, Jeffrey Cook; validation also **Ocelot Cane**).

### Forensic conclusion (Build 452 investigation)

| Observation | Meaning |
|-------------|---------|
| `kw_autonomy_mutation` Convert traces = **0** in session | No guarded callsite re-ConvertÃ¢â‚¬â„¢d those Sims during play |
| First census `post_load_fixup` already showed `KWAutonomy` | Stale per-Sim ownership present **before** `Main.Start` injection |
| `toggleRaw=True`, `toggleGuard=True` | Toggle active; guards working on **new** mutations |
| Manager global always EA in snapshots | Layer A global path OK; leak was **per-Sim instance** |

**Root cause class:** asymmetric fix Ã¢â‚¬â€ toggle prevented **new** KW takeover but did not **normalize** Sims that already held `KWAutonomy` at load. `FixSim()` only created EA autonomy when `Autonomy == null`; existing `KWAutonomy` was left untouched. Only `AutonomyManager.Uninstall()` had full revert logic (mod disable path).

**Contributing quit bug (manager only):** `Main.Stop()` previously called `AutonomyManager.Stop(true)` **only when toggle OFF**. With toggle ON after mid-session OFFÃ¢â€ â€™ON, KW manager could remain alive until process end Ã¢â‚¬â€ fixed separately (does **not** revert per-Sim types).

### Fix Ã¢â‚¬â€ shared revert helper + load integrity

Extracted from `Uninstall()` into reusable helpers in `Oniki.Gameplay/AutonomyManager.cs`:

| API | Behavior |
|-----|----------|
| `TryRevertSimAutonomyToEa(Sim, source)` | `new Autonomy()`; copy runtime wiring from `KWAutonomy` (`mActor`, `mMotives`, scorer, situation, queue timing, LOD, venues, override params, etc.); log `revert_to_ea` |
| `ApplyEaOwnershipSimIntegrity(source)` | If toggle ON, iterate `Household.AllSimsLivingInWorld()`, revert each CreatedSim with `KWAutonomy` |

**Hook points:**

| Lifecycle | Action |
|-----------|--------|
| `KinkyMod.PostLoadFixUp()` | `ApplyEaOwnershipSimIntegrity("post_load_fixup.pre")` **before** household fixup loop |
| `SimTools.FixSim()` | If toggle ON and autonomy already `KWAutonomy`, `TryRevertSimAutonomyToEa(..., "fixsim.sim")` |
| `AutonomyManager.Uninstall()` | Refactored to call `TryRevertSimAutonomyToEa` (unchanged player-visible disable-mod behavior) |

**Idle risk:** revert preserves actor/motive/scorer handoff (same pattern as legacy `Uninstall()`); validation showed autonomous commits on previously impacted Sims after revert.

### Fix Ã¢â‚¬â€ world quit manager hygiene

| File | Change |
|------|--------|
| `Oniki/Main.cs` `Stop()` | **Always** `Oniki.Gameplay.AutonomyManager.Stop(true)` Ã¢â‚¬â€ removed `if (!IsKWAutonomyManagerDisabled())` guard |

Ensures KW global manager shuts down cleanly on every world quit regardless of toggle state.

### Explicitly NOT shipped (deferred)

| Idea | Decision |
|------|----------|
| Revert all Sims on **world quit** | **Not added** Ã¢â‚¬â€ save flush usually happens before `Main.Stop`; redundant with load integrity for runtime; weak save hygiene |
| Revert on **toggle ON mid-session** | **Not added** Ã¢â‚¬â€ confirm dialog promises next load; optional future enhancement |
| Layer B singleton revert | **Out of scope** Ã¢â‚¬â€ see architecture section |

**Save hygiene note:** After one load + play session + **save**, reverted Sims should serialize through normal EA autonomy path; next cold load may show **0** `revert_to_ea` lines if save no longer rehydrates `KWAutonomy`.

---

## Validation status (tested, Build 452)

### Pre-fix export (`KWEAAutonomyLog_Export_159D5ECA.xml`)

- Global manager EA; **7** persistent `KWAutonomy` Sims; **246** RED_FLAG; **0** in-session Convert traces.

### Post-fix export (`KWEAAutonomyLog_Export_D22B2B49.xml`) Ã¢â‚¬â€ **PASS**

| Check | Result |
|-------|--------|
| `revert_to_ea` at `post_load_fixup.pre` | **8** Sims (7 historical + Ocelot Cane) |
| `kwAutonomyCount` all censuses | **0** / 68 Sims |
| `RED_FLAG` | **0** |
| Global manager | `Sims3.Gameplay.Autonomy.AutonomyManager` throughout |
| Autonomous activity | Previously impacted Sims show `autonomous_commit` / `autonomous_enqueue` with `autonomyType=Autonomy` (e.g. Tanner `PlayComputerGames` / `KWPlayGame`, Harry `VisitCommunityLot`, Katrina eat/reaction, Richie `VisitCommunityLot`, Bianca `GoHome`) |
| In-game | Confirmed autonomy appeared normal |

---

## KW Mail Carrier compatibility under EA autonomy ownership

### Background

- Testing found a service-specific regression with **KW Mail Carrier / postman** while `DisableKWAutonomyManager=ON`.
- The global ownership state was correct (`Sims3.Gameplay.Autonomy.AutonomyManager`, per-Sim `Autonomy`), but the KW mail carrier situation still assumed a `KWAutonomy` worker in its hang-around/social phase.
- Symptom: WooHoo with the spawned mail carrier could accept but fail before stage start because the target's `WooHooGoTo` disappeared from the queue; `KWWooHooSequenceLog_*` showed `participant_goto_missing_or_foreign` and `SynchronizationFailed` with `playedStages=0`.

### Fix

| Area | Change |
|------|--------|
| `KWHangAroundBeforeLeaving.Init()` | No longer resets `worker.Autonomy.mAutonomyDisabledCount` while the worker is already in a KW WooHoo flow |
| `OnSocializedWith()` / `PushSocialDelayed()` / `PushSocial()` | Suppresses mail-carrier social pushes while `WooHooSequence`, `WooHooGoTo`, `WooHooLoop`, `WooHooJoinGoTo`, or `WooHooJoinInstance` is current, queued, or in transition |
| `PushSocial()` | Replaced direct `(Autonomy as KWAutonomy).FindBestKinkySocial(...)` assumption with guarded use: use `KWAutonomy` when present, otherwise fall back to `SocializeProxy.Singleton_Situation` under EA autonomy |
| `TimeToLeave()` | Treats queued/transition WooHoo flow as a reason to delay leaving, not only `CurrentInteraction is IKinkyInteraction` |

### Follow-up: confine post-delivery autonomy to the KW social situation

#### User report and diagnosis

- A user reported that, after delivering the mail, the KW postman autonomously entered the household pool while **EA owns autonomy** (`DisableKWAutonomyManager=ON`).
- The report is consistent with the code path rather than an accidental routing failure:
  - on an active residential lot, `KWHangAroundBeforeLeaving.Init()` explicitly reset `worker.Autonomy.mAutonomyDisabledCount` to `0`;
  - it also forced high autonomy LOD with `SetIsHighAutonomyLOD(true)`;
  - under EA ownership, this exposed the service worker to the lot's general autonomous interaction catalogue, including pool, television, computer, bed, and other household-object interactions whose own tests allow service Sims.
- The EA `MailCarrierSituation.HangAroundBeforeLeaving` baseline only waits for the leave alarm and reacts to socialization; it does not explicitly enable unrestricted high-LOD local autonomy.
- Under KW ownership, the same open-autonomy window was less visible because `KWAutonomy` and the mail-carrier social callbacks kept the worker strongly biased toward social candidates. Repeated `Casual Talk` / `Chat` could occur, but the primary regression was the unrestricted EA object autonomy rather than social repetition itself.

#### Unified fix

| Area | Change |
|------|--------|
| `KWHangAroundBeforeLeaving.Init()` | On the active lot, if the worker is not already in a protected WooHoo flow and autonomy is currently enabled, the situation raises `mAutonomyDisabledCount` from `0` to `1` and records ownership of that increment |
| `KWHangAroundBeforeLeaving.Init()` | Removed the explicit `SetIsHighAutonomyLOD(true)` call |
| `OnSocializedWith()` | Removed the repeated high-autonomy-LOD promotion |
| Situation social flow | The postman remains socially active through the existing greeting, `OnSocializedWith`, `PushSocialDelayed`, `FindBestKinkySocial` when `KWAutonomy` is present, and `SocializeProxy.Singleton_Situation` fallback under EA autonomy |
| Situation social push window | Automatic mail-carrier social pushes are now limited to the first 2 Sim hours after the hang-around state starts; social interactions already running are allowed to finish normally |
| Social variety / continuity | No hard two-social limit, aggressive repeat blacklist, or new mail-carrier cooldown is imposed; `SocializeProxy` retains its normal 4-7-social conversation chain and existing friendly/romantic/kinky candidate selection |
| `CleanUp()` | Restores `mAutonomyDisabledCount` from `1` to `0` only when this state instance raised it, avoiding removal of an autonomy lock owned by another system |

#### Resulting behavior

- With either autonomy owner, the KW postman remains primarily **situation-directed and social** during the post-delivery hangout.
- With EA ownership, the worker is no longer released into unrestricted household-object autonomy, preventing out-of-context choices such as using the customer's pool.
- With KW ownership, the postman keeps the established social behavior rather than becoming artificially idle after a small fixed number of interactions.
- After the initial 2-hour social window, the situation stops creating new automatic social pushes, giving the existing leave alarm a clean chance to move the postman into `Leave` once the current interaction ends.
- Social repetition remains possible where the available valid social pool is narrow; this change intentionally does not treat repeated `Chat` / `Casual Talk` as equivalent to the unrestricted-object-autonomy defect.
- WooHoo queue/transition guards from the previous compatibility fix remain active.
- No global swimming interaction test, service-wide blacklist, parallel autonomy controller, or change to ordinary townie autonomy was added.

### Scope

- This is a **mail-carrier situation compatibility shim**, not a hidden re-enable of `KWAutonomyManager`.
- It does **not** convert the postman to `KWAutonomy` when EA ownership is enabled.
- It is intentionally scoped to the KW mail carrier's hang-around/social/leave logic, so normal townie WooHoo, selectable WooHoo, and unrelated EA autonomy behavior remain untouched.
- Applies to already-spawned mail carriers after loading the patched DLL because the guards live in the runtime situation code.

### Validation

- Log after patch (`KWWooHooSequenceLog_Export_075EF770.xml`) showed Tanner/Valeria mail-carrier WooHoo completed normally:
  - `playedStages=5`
  - `reason=Finished`
  - no `SynchronizationFailed`
- Follow-up autonomy/flow logs showed no mail-carrier queue churn or KW ownership violation; remaining activity was normal EA autonomy on a populated Bridgeport save.
- Follow-up confinement patch:
  - `Ensure-CsUtf8.ps1` passed with no conversion required;
  - `Project/build_experimental.ps1` completed successfully and emitted the experimental `Oniki_KinkyMod.dll`;
  - in-game validation is pending for both EA and KW autonomy ownership, including delivery, extended socialization, WooHoo interruption, no-resident/invalid-target handling, and timely leave behavior.

---

## Pie menu / debug NPC spawn: cold tests and cross-save injector hygiene

### Background

- Reproduced a long-standing same-process failure pattern: save or exit to main menu, then load a different save without closing the game.
- In the newly loaded save, clicking terrain / Sims could fail with repeated ScriptErrors and effectively disable normal pie-menu use.
- Latest capture showed the concrete crash path in KW Build 452:
  - `Terrain_SpawnNPC.GetInteractionName()` or `Terrain_SpawnNPC.InternalTest()`
  - `NPC.Get(...)`
  - `NPC.Create(...)`
  - `OutfitTools.CacheOutfits(...)`
  - `SimBuilder.BuildModel(...)`
  - `System.NotSupportedException: Attempting to yield in a non-yielding context!`
- This was not the old `Settings.TravelData` poisoning pattern: Build 452 already clears stale travel RAM on non-travel quit/load paths. The more likely state leak was KW's static interaction-injection catalog surviving main-menu transitions in the same TS3 process.

### Root cause class

| Layer | Problem |
|-------|---------|
| Debug terrain menu | `Terrain_SpawnNPC` did heavy NPC lookup / creation-adjacent work while the game was only building or testing the pie menu |
| Interaction injector | `InteractionInjector.__table` and `__locked` were static session state; after world quit, a different save could inherit the previous world's injection catalog unless the process was restarted |
| DebugMode design | Debug interactions are injected globally and hidden by `Test()` when `DebugMode` is OFF; therefore even debug-only definitions must be safe to enumerate in any save/context |

### Fix

| File | Change |
|------|--------|
| `Oniki.Interactions.Debug/Terrain_SpawnNPC.cs` | Made `InternalTest()` and `GetInteractionName()` cold: no `NPC.Get`, no `Fixup()`, no `NPC.Recreate()`, no outfit/model/cache work during pie-menu construction |
| same | Kept `NPC.Get`, validity fixup, recreate fallback, busy-situation check, and spawn/route behavior in `Run()` only, when the player actually executes the debug command |
| `Oniki.Interactions/InteractionInjector.cs` | `Clear()` now resets both `__table` and `__locked=false`, allowing a clean rebuild on the next world load |
| `Oniki/KinkyMod.cs` | World quit handler now calls `InteractionInjector.Clear()` after travel-data hygiene, so main-menu -> different save does not reuse the previous save's KW injection catalog |

### Expected behavior

- Normal player pie menus should no longer be able to crash because a debug NPC-spawn definition tries to build/cache an outfit while the menu is being enumerated.
- Debug **Spawn NPC** entries still exist when `DebugMode` is ON, but labels are simple role IDs until the command is executed.
- Actual NPC creation, fixup/recreate, outfit caching, Peeping/Callgirl/Escort/HighSchool busy checks, and spawn/routing behavior remain available in the action path.
- Loading a second save in the same game session rebuilds KW's injection catalog instead of carrying the previous save's static injector state forward.

### Validation

- No errors from the injector or `Terrain_SpawnNPC` changes.

---

## Premade KW NPC roster: Constance Prudence and Ingrid Van Houten

### Bugs

- `ONIKI_NPCs.package` contained 20 premade Sim templates (`SIME`) but only 19 active entries in its `NPCs` XML.
- Constance Prudence's template and `NPCNames` enum value existed, but her XML definition was commented out. She could appear in the Debug Spawn menu, while the runtime had no active NPC data from which to create her.
- Ingrid Van Houten had both a valid template and an active XML definition, but was missing from `NPCNames`. Because the Debug Spawn menu is generated from that enum, Ingrid could be created by KW systems but could not appear as a manual Spawn NPC entry.
- A failed `NPC.Get()` call in the actual spawn action returned silently, leaving no useful indication that NPC data or a template was unavailable.

### Fix

| File / package | Change |
|----------------|--------|
| `full_install/ONIKI_NPCs.package` | Reactivated the existing Constance Prudence XML definition (`0xB0DDE1CBB3561F26`) |
| `Oniki.Roles/NPCNames.cs` | Added `IngridVanHouten` with her existing template/XML ID `0xD6947D4770492286` |
| `Oniki.Interactions.Debug/Terrain_SpawnNPC.cs` | Added explicit spawn failure feedback when `NPC.Get()` returns `null`, including the requested NPC enum name and hexadecimal ID |

### Result

- The package now has 20 `SIME` resources and 20 active NPC XML definitions.
- Constance Prudence and Ingrid Van Houten are both represented consistently by template, XML data, and code enum.
- Both Sims can be listed under `Ground -> Kinky World -> Debug -> Spawn`.
- Missing or invalid NPC data no longer causes a completely silent Debug Spawn failure.
- No new setting, migration, tuning value, or STBL entry was required.

### Validation

- Package verification through the repository S3PE/s3pi libraries confirmed:
  - 20 `SIME` resources;
  - 20 active NPC XML rows;
  - exactly one active Constance definition;
  - exactly one active Ingrid definition;
  - all other package resource counts preserved.

---

## EA service NPC preservation and opt-in KW service situations

### Background

- KW uses `KWServices` as its global service manager, while each service type has a separate handler such as `MailCarrier`, `PizzaDelivery`, `Repairman`, or `Maid`.
- The KW subclasses create the corresponding KW Situation through `InternalCreateSituation()`.
- Legacy startup converted some service handlers by calling `KWServices.Clean<T>()` before constructing the KW replacement.
- Destroying the EA handler before transferring its state could discard its premade service pool and cause KW/EA to generate replacement workers.
- Pizza Delivery was converted unconditionally at every KW startup. Maid had a setting and implementation, but its startup call was disabled.

### New service model

| Setting state | Active service handler | Worker pool | Situation |
|---------------|------------------------|-------------|-----------|
| service toggle OFF | EA handler | existing EA/premade Sims preserved | EA Situation |
| service toggle ON | KW subclass | existing pool transferred intact | KW Situation |

- `KWServices` remains the single global manager because other KW systems rely on its service simulation and reference hygiene.
- Only one handler for each service type remains active after conversion.
- Conversion now works in both directions:
  - EA handler -> KW handler when the individual service toggle is enabled;
  - persisted KW handler -> EA handler when the toggle is disabled.
- The transfer occurs before the old handler is destroyed.

### State preserved during conversion

- service NPC pool;
- created Sim map;
- requested lots;
- preferred service NPC assignments;
- Sims assigned to lots;
- active Situation assignment map;
- last-assigned timestamps;
- move-in-service and worker-assignment flags;
- last-dismissed timestamp;
- `Service` and `CreatedByService` references on affected `SimDescription` objects.

The old handler receives empty replacement collections before `Service.Destroy()` runs, preventing transferred Sims and state from being treated as disposable ownership of the old service.

### Duplicate-service handling

- `KWServices.Start()` no longer blindly destroys an exact-class duplicate.
- Duplicate state is merged into the retained handler before the duplicate object is destroyed.
- `KWServices.AddSafe()` likewise merges same-`ServiceType` state instead of discarding the previous handler and its pool.
- Fixed `FixService()` so a newly initialized null pool is also used by the remainder of the validation pass.

### Individual services

- **Mail Carrier:** remains EA by default; enabling `KWMailCarrier` preserves the existing postmen and activates `KWMailCarrierSituation`.
- **Repairman:** remains EA by default; enabling `KWRepairman` preserves the existing repair workers and activates `KWRepairmanSituation`.
- **Maid:** `KWMaid.Start()` is active again, but `KWMaid=false` leaves/restores the EA handler. Enabling it preserves the existing maids and activates `KWMaidSituation`.
- **Pizza Delivery:** added a dedicated `KWPizzaDelivery` toggle. Its new default is `false`, so pizza delivery is no longer converted unconditionally.
- `CreateNewNPCForPool()` remains available only for normal service demand when the preserved pool cannot satisfy an assignment.

### New setting

- Full STBL key: `Oniki.KinkyMod.OptionSettingPizzaDelivery`
- Hash: `0x2D9350C50A185C98`
- EN: `Use Kinky World pizza delivery situations`
- Setting dictionary key: `KWPizzaDelivery`
- Default: `false`
- Migration/integrity: add-if-missing through `EnsureReleaseRequiredSettingsPresent()`; existing values are never overwritten.
- The entry was added to all 23 locale STBL resources in the MULTI package.

### Files

- `Oniki.Services/KWServices.cs`
- `Oniki.Services/KWMaid.cs`
- `Oniki.Services/KWMailCarrier.cs`
- `Oniki.Services/KWPizzaDelivery.cs`
- `Oniki.Services/KWRepairman.cs`
- `Oniki.UI/OptionSettingPizzaDelivery.cs`
- service toggle/gender UI files;
- `Oniki/Settings.cs`;
- `Oniki/Main.cs`;
- `Oniki_KinkyMod.csproj`;
- `full_install/ONIKI_KinkyMod.package`.

---

## Brothel schedule picker diagnostics (Russian MULTI investigation)

### Background

- User report on Build 451 MULTI: opening the brothel daily schedule grid for assigned employees / all employees could close the picker immediately in Russian.
- Same flow works in FULL-EN; exported Russian STBL rows for schedule headers/actions did not show obvious corruption.
- Build 452 adds targeted diagnostics only, so the next affected user test can produce a useful `ScriptError` / KW log instead of a silent picker close.

### Added diagnostics

| Area | Behavior |
|------|----------|
| Generic `OptionMenu.Show()` | Captures picker construction failures around headers, rows, dialog creation, callback setup, and `StartModal(true)` |
| Brothel `PlanningDay` grid | Enables picker breadcrumbs specifically for the 13-column brothel schedule table |
| Brothel schedule cells | Logs Sim, SimDescriptionId, day, column, interval, action, and localized cell text when `EnableGlobalBuffer` is ON |
| Exception path | Uses `Log.Write(exception, context)` so real failures create native TS3 `ScriptError` data even if normal buffer logging is disabled |

### Logging gates

- Breadcrumb / cell traces respect `EnableGlobalBuffer`.
- For best support captures, ask affected users to enable:
  - `EnableGlobalBuffer`
  - `LogKWErrorOnQuit`
- Real caught exceptions still go through KW's exception channel and should appear as TS3 `ScriptError` with the diagnostic context.

### Diagnostic context includes

- menu type/name, stage, column index, item index/name
- item count, preselected count, selectable row count, sortable flag, third button key/text
- brothel name, lot id, employee count, selected day
- per-cell Sim name/id, job/lot id on exception, schedule null state, interval/action

---

## WooHoo pie menu: selectable receivers can control active stage

### Background

- In active WooHoo loops, pie-menu access to **Kinky... -> WooHoo... -> Change position / Next stage** still depended on the internal master/receiver split.
- Result: when the currently selected Sim was a selectable receiver rather than the loop master, player-directed stage controls could disappear from the pie menu even though the keyboard shortcut path could still open the change-position picker.
- This made the UI feel inconsistent: player-selectable participants in the same active WooHoo stage did not have the same manual control surface.

### Fix

| Interaction | Change |
|-------------|--------|
| `WooHooChangePosition` | Removed the master-only runtime gate and the pie-menu veto for non-master participants when the master is human |
| `WooHooNextStage` | Removed the pie-menu veto for non-master participants when the master is human |
| Both | Require actor and clicked target to belong to the same `WooHooInstance`, so unrelated Sims cannot expose these controls |

### Player-visible behavior

- If the selected Sim is already participating in an active WooHoo loop, **Change position** and **Next stage** can appear for any selectable participant in that same loop when the normal gameplay conditions allow them.
- Works from the selected Sim / self-click and from clicking another participant in the same WooHoo sequence.
- Existing protection for `Victim == actor` remains unchanged.

---

## Optional male pubic hair support

### Background

- KW already had male pubic hair resource IDs in code, but the automatic pubic hair system still behaved effectively female-only in several runtime paths.
- The installed Build 452 package layout also makes male resources optional:
  - female pubic hair resources are present in `full_install/ONIKI_PubicHairs.package`;
  - male pubic hair resources are provided by `optional_addons/PubicHairMale_cmar.package`.
- Without the optional male package installed, enabling the code path cannot create visible male pubic hair CAS parts.

### New setting

- Added **Global Options -> Pubic Hairs -> Enable male pubic hairs**.
- Default is `OFF`, preserving the previous female-only behavior unless the player explicitly opts in.
- Male pubic hairs are active only when both settings are enabled:
  - **Automatic Pubic Hairs**;
  - **Enable male pubic hairs**.
- Female pubic hairs continue to use the existing **Automatic Pubic Hairs** master toggle and do not require the new male toggle.

### Behavior

- The pubic hair runtime gate is now centralized through `Settings.EnablePubicHairsFor(SimDescription)`.
- Automatic pubic hair assignment, growth/update passes, direct refresh, shaving interactions, and shower/bath shaving checks now use the same gender-aware gate.
- Male Sims can receive the same automatic pubic hair lifecycle as female Sims when:
  - the master pubic hair setting is ON;
  - the male toggle is ON;
  - compatible male pubic hair CAS resources are installed.
- When KW adds pubic hair without a saved preset, it now preserves the Sim's body-hair color instead of copying a hair-color channel.
- If an outfit already contains a `BodyHairStomach` CAS part, KW no longer adds a second pubic hair part to that same outfit. This avoids overriding player-authored Cmar/JVSmith body-hair choices and prevents custom body-hair color from being normalized during pubic hair refresh.
- The update confirmation shared by the master toggle and the male toggle now asks whether to update all Sims immediately.
  - **Yes** saves the setting and forces a pubic hair refresh.
  - **No** saves the setting but does not force an immediate refresh.

### UI and safety fixes

- Reworked the master pubic hair toggle and the male pubic hair toggle to use a safer picker/dialog flow.
- The settings picker is closed before the confirmation dialog opens, then reopened at the same menu after the dialog completes.
- Fixed a missing-key case where same-build saves could show the new toggle but fail to persist `Enabled`.
- Fixed `ForceUpdatePubicHairs()` null guards so Sims without an available `OutfitManager` do not throw during a forced refresh.
- Fixed the picker resume path so it defers `StartModal(true)` when the callback is running in a non-yielding context.

### Shave interaction follow-up

- Fixed `Shave` / pubic-hair removal so it no longer removes only the single pubic hair resource currently saved from `Naked:0`.
- KW now removes every CAS part whose key or parent key is registered in `FemalePubicHairKeys` or `MalePubicHairKeys` from all outfits visited by the outfit operation.
- The saved naked preset is still kept for regrowth, but the actual shave cleanup now covers mismatched pubic hair parts left on other outfits after CAS edits.
- This is especially relevant after Kinky Extended CAS body-hair editing, where different outfits can temporarily hold different KW-registered pubic hair resources.

### Kinky traits and scoring

- Existing hair preference scoring uses the target Sim's `OutfitManager.IsHairy` state, not a female-only check.
- With male pubic hair support enabled, hair preference traits can therefore evaluate male targets too.
- This includes both hairy and shaved preference paths once male Sims participate in the same pubic hair lifecycle.

### Settings migration

| Key | Default | `Upgrade()` | `EnsureReleaseRequiredSettingsPresent()` |
|-----|---------|-------------|------------------------------------------|
| `EnableMalePubicHairs` | `false` | `PreviousVersion < 452` -> add-if-missing | yes |

**Policy:** preserve existing serialized values; never overwrite on upgrade.

### STBL keys

- `Oniki.KinkyMod.OptionSettings.EnableMalePubicHairs`
- `Oniki.KinkyMod.OptionSettings.UpdatePubicHairQuestion`

### Source files

- `Oniki/Settings.cs`
- `Oniki.Gameplay/OutfitManager.cs`
- `Oniki.Utilities/OutfitTools.cs`
- `Oniki.Gameplay/SimData.cs`
- `Oniki.Interactions/AskToShavePubicHair.cs`
- `Oniki.Interactions/RequestShavePubicHair.cs`
- `Oniki.Interactions/ShavePubicHairs.cs`
- `Oniki.Interactions/TakeShower.cs`
- `Oniki.Interactions/TakeBath.cs`
- `Oniki.UI/OptionSettingEnablePubicHairs.cs`
- `Oniki.UI/OptionSettingEnableMalePubicHairs.cs`
- `Oniki.UI/OptionSettingMenuPubicHair.cs`

### Validation

- In-game testing confirmed the new toggle persists and male Sims can receive pubic hairs when the optional male CAS resources are installed.
- Follow-up ScriptErrors from forced refresh and picker resume were fixed.
- World runtime profiling after enabling the feature showed no pubic-hair-specific loop, retry storm, or exception signature.

---

## Settings migration (Build 452)

| Key | Default | `Upgrade()` | `EnsureReleaseRequiredSettingsPresent()` |
|-----|---------|-------------|------------------------------------------|
| `DisableKWAutonomyManager` | `Main.kDefaultDisableKWAutonomyManager` (`false`) | `PreviousVersion < 452` Ã¢â€ â€™ add-if-missing | yes |
| `EaAutonomyOwnershipLogging` | `false` | `PreviousVersion < 452` Ã¢â€ â€™ add-if-missing | yes |

**Policy:** preserve existing serialized values; never overwrite on upgrade.

---

## STBL (S3SE Ã¢â‚¬â€ package update required)

### Disable KW Autonomy Manager

- `Oniki.KinkyMod.OptionSettings.DisableKWAutonomyManager`
- `0x748A367BA1B19C98`
- EN: Disable KW Autonomy Manager (EA ownership)

- `Oniki.KinkyMod.OptionSettings.DisableKWAutonomyManager.Confirm`
- `0x2BFE40EE7DF9325E`
- EN: (single key Ã¢â‚¬â€ dialog text differs for enable vs disable in code fallback; enable explains next-world-load apply)

### EA Autonomy Ownership logging

- `Oniki.KinkyMod.OptionSettings.EaAutonomyOwnershipLogging`
- `0xB6322BE5C6CFEAFB`
- EN: EA Autonomy Ownership logging (suggested label Ã¢â‚¬â€ verify against deployed STR)

Uses existing shared enabled/disabled display keys where applicable (`OptionSettings.Enabled` / `OptionSettings.Disabled`).

---

## Unchanged (explicit)

- **`EnhancedBasicAutonomy`**, **`KWRunAutonomy`**, per-Sim autonomy level menus Ã¢â‚¬â€ separate from global manager toggle
- **KW PreLoad** interaction singleton replacements Ã¢â‚¬â€ still active with mod enabled
- **Brothel / Kraken / service** systems Ã¢â‚¬â€ only guarded at explicit Convert/create paths listed above; broader KW gameplay unchanged
- **Autonomy Debug Logging** channel semantics Ã¢â‚¬â€ restored to pre-452 independence from EA ownership logger
- **No retroactive save surgery** beyond load-time integrity when toggle ON Ã¢â‚¬â€ Sims already saved under Classic KW behavior unchanged until player reloads with toggle ON

---

## Player support note (non-technical)

- New **Miscellaneous** toggle lets you run KW with **EA deciding Sim movement and lot visits** (like KW Light autonomy ownership) while keeping KW content. Change applies after **reload / travel / return to main menu**, not instantly mid-play.
- Default remains **Classic KW autonomy** (`OFF`). Turn **ON** only if you want EA-style town circulation and lower autonomy CPU cost.
- Optional **EA Autonomy Ownership logging** (under Logging Features) writes a diagnostic file on quit to verify the toggle is working; normal players can leave it off.
- If you used the toggle ON on a save that previously ran Classic KW for a long time, Build 452 automatically fixes stray per-Sim KW autonomy objects on load Ã¢â‚¬â€ no manual step required. Saving once after that cleans future loads.

---

## Source files touched (reference)

| File | Role |
|------|------|
| `Oniki/Main.cs` | `kDefaultDisableKWAutonomyManager`, `IsKWAutonomyManagerDisabled`, startup guards, census hooks, `Stop()` always stops manager, audit delegate |
| `Oniki.Gameplay/AutonomyManager.cs` | Start guard, `TryRevertSimAutonomyToEa`, `ApplyEaOwnershipSimIntegrity`, Uninstall refactor |
| `Oniki/KinkyMod.cs` | PostLoadFixUp integrity, quit dump routing |
| `Oniki.Utilities/SimTools.cs` | Instantiate / FixSim guards + revert |
| `Oniki.Utilities/EaAutonomyOwnershipLog.cs` | Forensic logger (new) |
| `Oniki/Log.cs` | EA ownership buffer + dump/clear |
| `Oniki/Settings.cs` | New keys, migration, SetValue sync hook |
| `Oniki.UI/OptionMenu.cs` | Picker diagnostic context and exception capture |
| `Oniki.UI/OptionSettingDisableKWAutonomyManager.cs` | Toggle UI + confirm (FunctionTask pattern) |
| `Oniki.UI/OptionSettingMenuMisc.cs` | Menu wiring |
| `Oniki.Gameplay/BrothelManager.cs`, `Oniki.Situations/BrothelSituation.cs` | Convert guards; brothel schedule picker diagnostics |
| `Oniki.Situations/KWMailCarrierSituation.cs` | EA-ownership compatibility for KW mail carrier hang-around social pushes, WooHoo queue protection, and leave delay |
| `Oniki.Interactions/WooHooChangePosition.cs`, `Oniki.Interactions/WooHooNextStage.cs` | Player-directed stage controls no longer depend on master/receiver hierarchy |
| `ONIKI_KinkyModTuning XMLs/...Main%%+_XML.xml` | `kDefaultDisableKWAutonomyManager` |

---

## Drugs gameplay autonomy controls

### New menu

- Added **Global Options -> General Gameplay Settings -> Drugs Gameplay** immediately after **Autonomous Go Home interactions**.
- All three controls default to `OFF`.
- The controls apply to active Sims, selectable household Sims, and NPCs.
- Gates live in the interaction `Test(..., isAutonomous)` paths, so they apply whether EA autonomy or KW autonomy proposes the interaction.
- Player-directed drug interactions remain available.

### New settings

| Setting | Default | Autonomous behavior controlled |
|---------|---------|--------------------------------|
| `AllowAutonomousDrugSpiking` | `false` | Adding drugs/medicine effects to individual drinks or full bar trays |
| `AllowAutonomousDrugShopping` | `false` | `OnlineShoppingJunky` purchases from computers and smartphones |
| `AllowAutonomousDrugUse` | `false` | Taking drug-classified pills, smoking cannabis, and rolling joints |

Settings use add-if-missing integrity through `EnsureReleaseRequiredSettingsPresent()` and preserve existing serialized values.

### STBL (S3SE verified)

- `Oniki.KinkyMod.OptionSettings.MenuDrugGameplay.Label`
- `0x4442D9C3407A409E`
- EN: Drugs Gameplay

- `Oniki.KinkyMod.OptionSettings.AllowAutonomousDrugSpiking`
- `0x1378AE2F96FB934D`
- EN: Allow autonomous drink spiking

- `Oniki.KinkyMod.OptionSettings.AllowAutonomousDrugShopping`
- `0x47F5057B82978EDA`
- EN: Allow autonomous drug shopping

- `Oniki.KinkyMod.OptionSettings.AllowAutonomousDrugUse`
- `0x01FECE7D77B750CF`
- EN: Allow autonomous drug use

Code fallbacks provide the same English labels until the package STBL update is deployed.

---

## Diagnostics quick reference

| Goal | Settings | Export |
|------|----------|--------|
| Verify EA ownership after reload | `DisableKWAutonomyManager=ON` + `EaAutonomyOwnershipLogging=ON` + `EnableGlobalBuffer=ON` | `KWEAAutonomyLog_Export_*.xml` |
| Clean pass criteria | `kwAutonomyCount=0`, no `[RED_FLAG]`, manager type EA, impacted Sims show autonomous commits | |
| Legacy KW autonomy debug | `AutonomyDebugLogging=ON` | `KWAutonomyDebug_*` (separate channel) |

---

## Kinky Sleeping: stuck sleeper after horny dream / Sleep WooHoo cancellation

### Community report and confirmed ScriptErrors

- A horny Sim sleeping beside another Sim could show `BedSleep:TooHornyToSleep`, drop game speed from 3 to 1, then remain permanently in `SleepingPosture`.
- Canceling sleep did not recover the Sim; MasterController reset or Overwatch/ErrorTrap recovery was required.
- The player also reported Kinky Dream notifications, confirming that the active path was the custom `KWBedSleep` dream/Sleep WooHoo flow rather than vanilla sleep.
- Two ErrorTrap logs from the same save confirmed `System.NullReferenceException` in `KWBedSleep.Cleanup()` while:
  - the affected Sim was still in `SleepingPosture`;
  - `GetOutOfBed` was already queued;
  - the interaction queue was dequeuing the completed/canceled `KWBedSleep`.
- Missing **Into the Future** is not the cause. The failure is in paired Sleep WooHoo teardown and does not require EP11 functionality.

### Root cause

- `Cleanup()` assumed that `mIsWooHooing` guaranteed a live `mSpooningMasterInteraction` and called `StopWooHoo()` through an unchecked cast.
- Cancellation can remove or invalidate one participant before the partner cleanup runs, leaving `mIsWooHooing` true but the master reference, synchronization target, `SimData`, posture state machine, or outfit manager unavailable.
- One cleanup exception aborted the remainder of the method before `base.Cleanup()`, allowing the Sim to remain associated with the sleeping posture even though `GetOutOfBed` was queued.
- The slave wait loop also dereferenced `Actor.SynchronizationTarget.CurrentInteraction` without checking whether the synchronization target had already disappeared.
- `PostWooHoo()` contained an inverted null check and attempted to set `mWooHooed` only when the cast slave interaction was null.

### Fix

- Hardened `KWBedSleep.Cleanup()` as a best-effort teardown:
  - temporary sleeper pie interactions, privacy exit, paired WooHoo stop, and outfit-helper disposal are isolated so one failure does not suppress the remaining cleanup;
  - missing master interaction now falls back to clearing the local WooHoo flag, synchronization data, and spooning context;
  - `base.Cleanup()` is guaranteed through `finally`.
- Hardened `StopWooHoo()` against missing/destroyed actors, missing `SimData`, missing outfit managers, invalid slave casts, and missing posture state machines.
- Both valid participants now clear synchronization data during paired teardown.
- Added null/destroyed guards around the slave-side synchronization wait loop.
- Corrected the `PostWooHoo()` slave null check (`kWBedSleep != null`).

### Scope

- No automatic Sim reset, watchdog timer, or forced wake controller was added.
- No changes to Kinky Dream probability, horny thresholds, WooHoo acceptance, Dominant/Submissive relationship rules, or active/passive Sleep WooHoo roles.
- No new settings, migrations, tuning values, or STBL entries.

### Source file

- `Oniki.Interactions/KWBedSleep.cs`

### Follow-up: freeze before cleanup during Kinky Dream pairing

- Reproduction after the first cleanup hardening:
  1. Sim A sleeps horny and completes at least one dream masturbation;
  2. Sim B sleeps horny and begins dream masturbation;
  3. `BedSleep:TooHornyToSleep` appears;
  4. both Sims remain motionless inside the unchanged sleep interactions and never reach cleanup.
- Existing logs contained no exception or `KWBedSleep` transition detail, confirming that the failure could occur inside the paired state-machine handshake without reaching the ErrorTrap cleanup signature.

#### Additional root causes

- Autonomous Kinky Dream role inference could leave both `mActorSub` and `mTargetSub` false, especially for same-sex pairs without a Submissive/Love Big Cock trait.
- `SetWooHooParameters()` only assigns the `KWBedSocials` actors and clips when one of those role flags is true. With both false, the code still requested `WooHooHold` and entered `KWBedSocials`, producing an incomplete state-machine setup capable of freezing both sleepers.
- Normal sleep uses EA `SleepStage`, whose completion test returns complete immediately when Energy reaches maximum. A nearly-rested Sim could therefore end the sleep stage during the outfit/state-machine handshake even though the paired sexual animation still needed time to complete.
- Sleep WooHoo deadlines reused the general dream-loop `mTimePassed`, but the Kinky Dream branch resets that timer immediately after `TryWooHooInBed()`. Handshake timeout, progression, and completion could therefore run against a shifted time base.
- `TryWooHooInBed()` returned `true` after finding an otherwise eligible partner even when either `AcceptWooHoo` call rejected the pairing. `UpdateDream()` then executed its success transition despite no `KWBedSocials` state machine ever starting.

#### Follow-up fix

- Added deterministic role normalization before every Sleep WooHoo start:
  - when neither inferred role is selected, use the same target-sub orientation as the player-facing active Sleep WooHoo definition;
  - guarantees that `SetWooHooParameters()` assigns valid `x/y` actors and animation clips.
- Once Sleep WooHoo is accepted, both participants move from energy-bound `SleepStage` to a bounded `TimedStage` covering the handshake and animation window.
  - Energy may reach maximum without terminating the paired animation.
  - The change applies only after Sleep WooHoo acceptance; ordinary sleep remains EA energy-driven.
- Handshake readiness timeout now cancels and exits both participants for all Sleep WooHoo types, rather than leaving normal Kinky Dream sleep running with a failed pairing.
- Exceptions during `UpdateWooHoo()` now abort the paired context and wake the initiating interaction instead of leaving `mIsWooHooing` active indefinitely.
- Added a dedicated per-session `mSleepWooHooElapsed` clock. Pairing start, timeout, progression, and completion no longer depend on the dream/thought-balloon timer being reset.
- `TryWooHooInBed()` now reports success only after `StartWooHoo()` actually runs; mutual rejection remains a normal dream/sleep path.

#### General Logging diagnostics

- Added `[KW-SLEEP-WOOHOO]` events to the existing `General_Logging_` export.
- Enable:
  - `Miscellaneous -> Logging Features -> Miscellaneous Logging`
  - `Enable Global Buffer`
- Traced phases include:
  - `accepted`
  - `start`
  - `handshake_waiting`
  - `handshake_ready`
  - `smc_loop_started`
  - `handshake_timeout`
  - `update_exception`
  - `stop_begin`
  - `stop_end`
  - `attempt`
  - `attempt_blocked`
  - `attempt_rejected`
  - `loop_exit`
  - `force_stop_begin`
  - `force_stop_end`
- Records include both Sims, Energy, remaining stage time, inferred roles, category, readiness, and pairing state.
- No new setting or STBL key was required.

#### Reset / world-quit cleanup

- `Cleanup()` no longer calls the animated/yielding `StopWooHoo()` path.
- Added `ForceStopWooHooNoYield()` for reset and world-quit cleanup:
  - no `StateMachineClient.RequestState(...)`;
  - no animation transition or sleep-state request;
  - clears paired flags, cancellation state, synchronization data, and spooning references for both participants.
- This prevents `Attempting to yield in a non-yielding context!` when an active Sleep WooHoo is reset or the world is closed.

---

## Kraken route abort and repeat-attack cooldown hardening

### Community report

- A Sim living on a houseboat and swimming frequently could be attacked by the Kraken repeatedly, including reports of multiple attacks within roughly 30 Sim minutes.
- The same play session produced repeated `ScriptError` files with:

```
Sims3.SimIFace.SacsAbortException: Request was interrupted. (actor = 'x', state = 'Swim')
Kraken.DoRouteSegment -> Kraken.DoRoute -> Kraken.Simulate
```

### Root cause

- `Kraken.DoRouteSegment()` requested the solo Kraken state-machine state `Swim` for every route segment without treating SACS interruption as a normal route abort.
- If the player, Sim queue, posture, ocean routing, or engine interrupted the Kraken swim state, the abort bubbled to the outer simulation catch and was exported as a ScriptError.
- The per-Sim `krakenattack` cooldown was previously applied inside `KrakenAttack.Run()`. If the queued attack was interrupted before that path ran far enough, the same Sim could remain eligible and be selected again almost immediately.

### Fix

- `DoRouteSegment()` now catches expected SACS route-state failures (`SacsAbortException`, `SacsErrorException`, `SacsTimeoutException`) around `RequestState("Swim")` and returns route failure instead of exporting a ScriptError.
- A failed follow route now clears the current route/prey and puts the Kraken into a short sleep before it can search again.
- `StartAttack()` now performs a final `OffCooldown("krakenattack", null)` check immediately before queuing the attack.
- The `krakenattack` cooldown is applied as soon as the attack is queued, not only after `KrakenAttack.Run()` begins.
- The existing tunable cooldown value remains `kAttackCooldown = 12f`; this is a `SimData` interaction cooldown and is measured in Sim hours.
- When `Rapes` is disabled and a Kraken vaginal phase counts as the Sim's first physical WooHoo, the reward now uses the canonical `KWBuff.FirstWooHoo` moodlet with `Origins.FromWooHoo`, records the Sim's first-WooHoo flag, and resolves the existing `Oniki.KinkyMod.BuffWooHoo:Description_Level2_FirstWooHoo` text instead of reusing the rape-specific first-WooHoo buff with a positive mood value.

### Scope

- No change to Kraken spawn eligibility, attack chance, attack animation flow, pregnancy gate, rape/consensual moodlet logic, or the player-facing `KrakenEnabled` / `KrakenPregnancyEnabled` settings.
- Attacks are not disabled permanently: once the 12 Sim-hour cooldown expires, the same Sim can become eligible again through the existing selection path.

### Source file

- `Sims3.Gameplay.Objects.OnikiStuff/Kraken.cs`

---

## Comic book reading: Nerd influence grant no longer resets Sims through ErrorTrap

### Community report

- A Sim could be reset by ErrorTrap when finishing a KW `ReadBook` interaction on a comic book, with the stack ending in `Book_ReadBook.KWReadLoop()` -> `Skill.AddPoints()` -> `SkillManager.UpdateSkillUI(...)`.

### Fix

- The post-read comic-book Nerd influence reward is now isolated from the main reading interaction.
- KW now retrieves the `InfluenceNerd` skill explicitly, checks for a null result, and grants the comic reward with the non-UI-refreshing `AddPoints(points, true, false)` overload.
- If the Sims 3 skill/UI layer still throws while granting that small bonus, KW catches only that grant failure and writes it to the KW error log instead of letting ErrorTrap reset the Sim.
- Follow-up: `Book_ReadBook.PreLoad()` now fully replaces EA `ReadBook.Definition` tuning with the KW definition instead of cloning a parallel KW tuning while leaving the EA tuning active.
- Follow-up: during preload, already-instantiated `Book` objects are repaired by removing exact EA `ReadBook.Definition` interaction pairs from both the normal book interaction list and the inventory interaction list, then adding the KW `Book_ReadBook.Definition` variant when missing.
- Follow-up hardening: the same replacement/repair now covers `ReadBookChooser.Definition` and `GetBook.Definition`, because the beach-towel/inventory path depends on the KW chooser to keep the Sim in posture before pushing `Book_ReadBook`.
- Follow-up hardening: the book interaction repair now also runs from `KinkyMod.PostLoadFixUp()` after world load, so saved books and inventory books that are materialized after preload are repaired too.

### Scope

- No change to normal book reading, comic-book reading flow, book completion, WooHoo/read-on-beach-towel behavior, or outfit handling.
- Inventory reading and beach-towel reading are now explicitly covered by KW's chooser/get/read chain; this prevents comic books launched from inventory from bypassing the protected Nerd influence grant through vanilla `Sims3.Gameplay.Objects.ReadBook.RunFromInventory()`.
- The worst-case fallback is that the Sim finishes reading normally but misses the small Nerd influence reward for that comic read.

### Source file

- `Oniki.Interactions/Book_ReadBook.cs`
- `Oniki.Interactions/Book_ReadBookChooser.cs`
- `Oniki.Interactions/Book_GetBook.cs`
- `Oniki/KinkyMod.cs`
